mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-03-26 17:51:54 +01:00
Personal assistants
This commit is contained in:
parent
f616b7e6e5
commit
b407edbe49
@ -24,7 +24,7 @@ def load_prompts_from_yaml(prompts_yaml: str = PROMPTS_YAML) -> None:
|
||||
with Session(get_sqlalchemy_engine()) as db_session:
|
||||
for prompt in all_prompts:
|
||||
upsert_prompt(
|
||||
user_id=None,
|
||||
user=None,
|
||||
prompt_id=prompt.get("id"),
|
||||
name=prompt["name"],
|
||||
description=prompt["description"].strip(),
|
||||
@ -34,7 +34,6 @@ def load_prompts_from_yaml(prompts_yaml: str = PROMPTS_YAML) -> None:
|
||||
datetime_aware=prompt.get("datetime_aware", True),
|
||||
default_prompt=True,
|
||||
personas=None,
|
||||
shared=True,
|
||||
db_session=db_session,
|
||||
commit=True,
|
||||
)
|
||||
@ -67,9 +66,7 @@ def load_personas_from_yaml(
|
||||
prompts: list[PromptDBModel | None] | None = None
|
||||
else:
|
||||
prompts = [
|
||||
get_prompt_by_name(
|
||||
prompt_name, user_id=None, shared=True, db_session=db_session
|
||||
)
|
||||
get_prompt_by_name(prompt_name, user=None, db_session=db_session)
|
||||
for prompt_name in prompt_set_names
|
||||
]
|
||||
if any([prompt is None for prompt in prompts]):
|
||||
@ -80,7 +77,7 @@ def load_personas_from_yaml(
|
||||
|
||||
p_id = persona.get("id")
|
||||
upsert_persona(
|
||||
user_id=None,
|
||||
user=None,
|
||||
# Negative to not conflict with existing personas
|
||||
persona_id=(-1 * p_id) if p_id is not None else None,
|
||||
name=persona["name"],
|
||||
@ -96,7 +93,6 @@ def load_personas_from_yaml(
|
||||
prompts=cast(list[PromptDBModel] | None, prompts),
|
||||
document_sets=doc_sets,
|
||||
default_persona=True,
|
||||
shared=True,
|
||||
is_public=True,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
@ -12,6 +12,7 @@ from sqlalchemy import update
|
||||
from sqlalchemy.exc import MultipleResultsFound
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from danswer.auth.schemas import UserRole
|
||||
from danswer.configs.chat_configs import HARD_DELETE_CHATS
|
||||
from danswer.configs.constants import MessageType
|
||||
from danswer.db.constants import SLACK_BOT_PERSONA_PREFIX
|
||||
@ -27,6 +28,7 @@ from danswer.db.models import Prompt
|
||||
from danswer.db.models import SearchDoc
|
||||
from danswer.db.models import SearchDoc as DBSearchDoc
|
||||
from danswer.db.models import StarterMessage
|
||||
from danswer.db.models import User
|
||||
from danswer.db.models import User__UserGroup
|
||||
from danswer.llm.override_models import LLMOverride
|
||||
from danswer.llm.override_models import PromptOverride
|
||||
@ -313,13 +315,16 @@ def set_as_latest_chat_message(
|
||||
|
||||
def get_prompt_by_id(
|
||||
prompt_id: int,
|
||||
user_id: UUID | None,
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
include_deleted: bool = False,
|
||||
) -> Prompt:
|
||||
stmt = select(Prompt).where(
|
||||
Prompt.id == prompt_id, or_(Prompt.user_id == user_id, Prompt.user_id.is_(None))
|
||||
)
|
||||
stmt = select(Prompt).where(Prompt.id == prompt_id)
|
||||
|
||||
# if user is not specified OR they are an admin, they should
|
||||
# have access to all prompts, so this where clause is not needed
|
||||
if user and user.role != UserRole.ADMIN:
|
||||
stmt = stmt.where(or_(Prompt.user_id == user.id, Prompt.user_id.is_(None)))
|
||||
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Prompt.deleted.is_(False))
|
||||
@ -351,14 +356,16 @@ def get_default_prompt() -> Prompt:
|
||||
|
||||
def get_persona_by_id(
|
||||
persona_id: int,
|
||||
# if user_id is `None` assume the user is an admin or auth is disabled
|
||||
user_id: UUID | None,
|
||||
# if user is `None` assume the user is an admin or auth is disabled
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
include_deleted: bool = False,
|
||||
) -> Persona:
|
||||
stmt = select(Persona).where(Persona.id == persona_id)
|
||||
if user_id is not None:
|
||||
stmt = stmt.where(or_(Persona.user_id == user_id, Persona.user_id.is_(None)))
|
||||
|
||||
# if user is an admin, they should have access to all Personas
|
||||
if user is not None and user.role != UserRole.ADMIN:
|
||||
stmt = stmt.where(or_(Persona.user_id == user.id, Persona.user_id.is_(None)))
|
||||
|
||||
if not include_deleted:
|
||||
stmt = stmt.where(Persona.deleted.is_(False))
|
||||
@ -397,33 +404,33 @@ def get_personas_by_ids(
|
||||
|
||||
|
||||
def get_prompt_by_name(
|
||||
prompt_name: str, user_id: UUID | None, shared: bool, db_session: Session
|
||||
prompt_name: str, user: User | None, db_session: Session
|
||||
) -> Prompt | None:
|
||||
"""Cannot do shared and user owned simultaneously as there may be two of those"""
|
||||
stmt = select(Prompt).where(Prompt.name == prompt_name)
|
||||
if shared:
|
||||
stmt = stmt.where(Prompt.user_id.is_(None))
|
||||
else:
|
||||
stmt = stmt.where(Prompt.user_id == user_id)
|
||||
|
||||
# if user is not specified OR they are an admin, they should
|
||||
# have access to all prompts, so this where clause is not needed
|
||||
if user and user.role != UserRole.ADMIN:
|
||||
stmt = stmt.where(Prompt.user_id == user.id)
|
||||
|
||||
result = db_session.execute(stmt).scalar_one_or_none()
|
||||
return result
|
||||
|
||||
|
||||
def get_persona_by_name(
|
||||
persona_name: str, user_id: UUID | None, shared: bool, db_session: Session
|
||||
persona_name: str, user: User | None, db_session: Session
|
||||
) -> Persona | None:
|
||||
"""Cannot do shared and user owned simultaneously as there may be two of those"""
|
||||
"""Admins can see all, regular users can only fetch their own.
|
||||
If user is None, assume the user is an admin or auth is disabled."""
|
||||
stmt = select(Persona).where(Persona.name == persona_name)
|
||||
if shared:
|
||||
stmt = stmt.where(Persona.user_id.is_(None))
|
||||
else:
|
||||
stmt = stmt.where(Persona.user_id == user_id)
|
||||
if user and user.role != UserRole.ADMIN:
|
||||
stmt = stmt.where(Persona.user_id == user.id)
|
||||
result = db_session.execute(stmt).scalar_one_or_none()
|
||||
return result
|
||||
|
||||
|
||||
def upsert_prompt(
|
||||
user_id: UUID | None,
|
||||
user: User | None,
|
||||
name: str,
|
||||
description: str,
|
||||
system_prompt: str,
|
||||
@ -431,7 +438,6 @@ def upsert_prompt(
|
||||
include_citations: bool,
|
||||
datetime_aware: bool,
|
||||
personas: list[Persona] | None,
|
||||
shared: bool,
|
||||
db_session: Session,
|
||||
prompt_id: int | None = None,
|
||||
default_prompt: bool = True,
|
||||
@ -440,9 +446,7 @@ def upsert_prompt(
|
||||
if prompt_id is not None:
|
||||
prompt = db_session.query(Prompt).filter_by(id=prompt_id).first()
|
||||
else:
|
||||
prompt = get_prompt_by_name(
|
||||
prompt_name=name, user_id=user_id, shared=shared, db_session=db_session
|
||||
)
|
||||
prompt = get_prompt_by_name(prompt_name=name, user=user, db_session=db_session)
|
||||
|
||||
if prompt:
|
||||
if not default_prompt and prompt.default_prompt:
|
||||
@ -463,7 +467,7 @@ def upsert_prompt(
|
||||
else:
|
||||
prompt = Prompt(
|
||||
id=prompt_id,
|
||||
user_id=None if shared else user_id,
|
||||
user_id=user.id if user else None,
|
||||
name=name,
|
||||
description=description,
|
||||
system_prompt=system_prompt,
|
||||
@ -485,7 +489,7 @@ def upsert_prompt(
|
||||
|
||||
|
||||
def upsert_persona(
|
||||
user_id: UUID | None,
|
||||
user: User | None,
|
||||
name: str,
|
||||
description: str,
|
||||
num_chunks: float,
|
||||
@ -496,7 +500,6 @@ def upsert_persona(
|
||||
document_sets: list[DBDocumentSet] | None,
|
||||
llm_model_version_override: str | None,
|
||||
starter_messages: list[StarterMessage] | None,
|
||||
shared: bool,
|
||||
is_public: bool,
|
||||
db_session: Session,
|
||||
persona_id: int | None = None,
|
||||
@ -507,7 +510,7 @@ def upsert_persona(
|
||||
persona = db_session.query(Persona).filter_by(id=persona_id).first()
|
||||
else:
|
||||
persona = get_persona_by_name(
|
||||
persona_name=name, user_id=user_id, shared=shared, db_session=db_session
|
||||
persona_name=name, user=user, db_session=db_session
|
||||
)
|
||||
|
||||
if persona:
|
||||
@ -539,7 +542,7 @@ def upsert_persona(
|
||||
else:
|
||||
persona = Persona(
|
||||
id=persona_id,
|
||||
user_id=None if shared else user_id,
|
||||
user_id=user.id if user else None,
|
||||
is_public=is_public,
|
||||
name=name,
|
||||
description=description,
|
||||
@ -566,24 +569,20 @@ def upsert_persona(
|
||||
|
||||
def mark_prompt_as_deleted(
|
||||
prompt_id: int,
|
||||
user_id: UUID | None,
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
prompt = get_prompt_by_id(
|
||||
prompt_id=prompt_id, user_id=user_id, db_session=db_session
|
||||
)
|
||||
prompt = get_prompt_by_id(prompt_id=prompt_id, user=user, db_session=db_session)
|
||||
prompt.deleted = True
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def mark_persona_as_deleted(
|
||||
persona_id: int,
|
||||
user_id: UUID | None,
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
persona = get_persona_by_id(
|
||||
persona_id=persona_id, user_id=user_id, db_session=db_session
|
||||
)
|
||||
persona = get_persona_by_id(persona_id=persona_id, user=user, db_session=db_session)
|
||||
persona.deleted = True
|
||||
db_session.commit()
|
||||
|
||||
@ -621,9 +620,7 @@ def update_persona_visibility(
|
||||
is_visible: bool,
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
persona = get_persona_by_id(
|
||||
persona_id=persona_id, user_id=None, db_session=db_session
|
||||
)
|
||||
persona = get_persona_by_id(persona_id=persona_id, user=None, db_session=db_session)
|
||||
persona.is_visible = is_visible
|
||||
db_session.commit()
|
||||
|
||||
|
@ -736,7 +736,6 @@ class Prompt(Base):
|
||||
__tablename__ = "prompt"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
# If not belong to a user, then it's shared
|
||||
user_id: Mapped[UUID | None] = mapped_column(ForeignKey("user.id"), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
description: Mapped[str] = mapped_column(String)
|
||||
@ -770,7 +769,6 @@ class Persona(Base):
|
||||
__tablename__ = "persona"
|
||||
|
||||
id: Mapped[int] = mapped_column(primary_key=True)
|
||||
# If not belong to a user, then it's shared
|
||||
user_id: Mapped[UUID | None] = mapped_column(ForeignKey("user.id"), nullable=True)
|
||||
name: Mapped[str] = mapped_column(String)
|
||||
description: Mapped[str] = mapped_column(String)
|
||||
@ -824,7 +822,7 @@ class Persona(Base):
|
||||
back_populates="personas",
|
||||
)
|
||||
# Owner
|
||||
user: Mapped[User] = relationship("User", back_populates="personas")
|
||||
user: Mapped[User | None] = relationship("User", back_populates="personas")
|
||||
# Other users with access
|
||||
users: Mapped[list[User]] = relationship(
|
||||
"User",
|
||||
|
@ -6,6 +6,7 @@ from sqlalchemy.orm import Session
|
||||
from danswer.db.chat import get_prompts_by_ids
|
||||
from danswer.db.chat import upsert_persona
|
||||
from danswer.db.document_set import get_document_sets_by_ids
|
||||
from danswer.db.models import Persona__User
|
||||
from danswer.db.models import User
|
||||
from danswer.server.features.persona.models import CreatePersonaRequest
|
||||
from danswer.server.features.persona.models import PersonaSnapshot
|
||||
@ -21,9 +22,19 @@ def make_persona_private(
|
||||
group_ids: list[int] | None,
|
||||
db_session: Session,
|
||||
) -> None:
|
||||
if user_ids is not None:
|
||||
db_session.query(Persona__User).filter(
|
||||
Persona__User.persona_id == persona_id
|
||||
).delete(synchronize_session="fetch")
|
||||
|
||||
for user_uuid in user_ids:
|
||||
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
|
||||
|
||||
db_session.commit()
|
||||
|
||||
# May cause error if someone switches down to MIT from EE
|
||||
if user_ids or group_ids:
|
||||
raise NotImplementedError("Danswer MIT does not support private Document Sets")
|
||||
if group_ids:
|
||||
raise NotImplementedError("Danswer MIT does not support private Personas")
|
||||
|
||||
|
||||
def create_update_persona(
|
||||
@ -32,8 +43,6 @@ def create_update_persona(
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
) -> PersonaSnapshot:
|
||||
user_id = user.id if user is not None else None
|
||||
|
||||
# Permission to actually use these is checked later
|
||||
document_sets = list(
|
||||
get_document_sets_by_ids(
|
||||
@ -51,7 +60,7 @@ def create_update_persona(
|
||||
try:
|
||||
persona = upsert_persona(
|
||||
persona_id=persona_id,
|
||||
user_id=user_id,
|
||||
user=user,
|
||||
name=create_persona_request.name,
|
||||
description=create_persona_request.description,
|
||||
num_chunks=create_persona_request.num_chunks,
|
||||
@ -62,7 +71,6 @@ def create_update_persona(
|
||||
document_sets=document_sets,
|
||||
llm_model_version_override=create_persona_request.llm_model_version_override,
|
||||
starter_messages=create_persona_request.starter_messages,
|
||||
shared=create_persona_request.shared,
|
||||
is_public=create_persona_request.is_public,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
@ -49,7 +49,7 @@ def create_slack_bot_persona(
|
||||
# create/update persona associated with the slack bot
|
||||
persona_name = _build_persona_name(channel_names)
|
||||
persona = upsert_persona(
|
||||
user_id=None, # Slack Bot Personas are not attached to users
|
||||
user=None, # Slack Bot Personas are not attached to users
|
||||
persona_id=existing_persona_id,
|
||||
name=persona_name,
|
||||
description="",
|
||||
@ -61,7 +61,6 @@ def create_slack_bot_persona(
|
||||
document_sets=document_sets,
|
||||
llm_model_version_override=None,
|
||||
starter_messages=None,
|
||||
shared=True,
|
||||
is_public=True,
|
||||
default_persona=False,
|
||||
db_session=db_session,
|
||||
|
@ -173,7 +173,7 @@ def stream_answer_objects(
|
||||
prompt = None
|
||||
if query_req.prompt_id is not None:
|
||||
prompt = get_prompt_by_id(
|
||||
prompt_id=query_req.prompt_id, user_id=user_id, db_session=db_session
|
||||
prompt_id=query_req.prompt_id, user=user, db_session=db_session
|
||||
)
|
||||
if prompt is None:
|
||||
if not chat_session.persona.prompts:
|
||||
|
@ -28,35 +28,6 @@ admin_router = APIRouter(prefix="/admin/persona")
|
||||
basic_router = APIRouter(prefix="/persona")
|
||||
|
||||
|
||||
@admin_router.post("")
|
||||
def create_persona(
|
||||
create_persona_request: CreatePersonaRequest,
|
||||
user: User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> PersonaSnapshot:
|
||||
return create_update_persona(
|
||||
persona_id=None,
|
||||
create_persona_request=create_persona_request,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.patch("/{persona_id}")
|
||||
def update_persona(
|
||||
persona_id: int,
|
||||
update_persona_request: CreatePersonaRequest,
|
||||
user: User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> PersonaSnapshot:
|
||||
return create_update_persona(
|
||||
persona_id=persona_id,
|
||||
create_persona_request=update_persona_request,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
class IsVisibleRequest(BaseModel):
|
||||
is_visible: bool
|
||||
|
||||
@ -92,19 +63,6 @@ def patch_persona_display_priority(
|
||||
)
|
||||
|
||||
|
||||
@admin_router.delete("/{persona_id}")
|
||||
def delete_persona(
|
||||
persona_id: int,
|
||||
user: User | None = Depends(current_admin_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
mark_persona_as_deleted(
|
||||
persona_id=persona_id,
|
||||
user_id=user.id if user is not None else None,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@admin_router.get("")
|
||||
def list_personas_admin(
|
||||
_: User | None = Depends(current_admin_user),
|
||||
@ -124,6 +82,48 @@ def list_personas_admin(
|
||||
"""Endpoints for all"""
|
||||
|
||||
|
||||
@basic_router.post("")
|
||||
def create_persona(
|
||||
create_persona_request: CreatePersonaRequest,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> PersonaSnapshot:
|
||||
return create_update_persona(
|
||||
persona_id=None,
|
||||
create_persona_request=create_persona_request,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@basic_router.patch("/{persona_id}")
|
||||
def update_persona(
|
||||
persona_id: int,
|
||||
update_persona_request: CreatePersonaRequest,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> PersonaSnapshot:
|
||||
return create_update_persona(
|
||||
persona_id=persona_id,
|
||||
create_persona_request=update_persona_request,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@basic_router.delete("/{persona_id}")
|
||||
def delete_persona(
|
||||
persona_id: int,
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> None:
|
||||
mark_persona_as_deleted(
|
||||
persona_id=persona_id,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
|
||||
@basic_router.get("")
|
||||
def list_personas(
|
||||
user: User | None = Depends(current_user),
|
||||
@ -148,7 +148,7 @@ def get_persona(
|
||||
return PersonaSnapshot.from_model(
|
||||
get_persona_by_id(
|
||||
persona_id=persona_id,
|
||||
user_id=user.id if user is not None else None,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
)
|
||||
@ -194,9 +194,9 @@ GPT_3_5_TURBO_MODEL_VERSIONS = [
|
||||
]
|
||||
|
||||
|
||||
@admin_router.get("/utils/list-available-models")
|
||||
@basic_router.get("/utils/list-available-models")
|
||||
def list_available_model_versions(
|
||||
_: User | None = Depends(current_admin_user),
|
||||
_: User | None = Depends(current_user),
|
||||
) -> list[str]:
|
||||
# currently only support selecting different models for OpenAI
|
||||
if GEN_AI_MODEL_PROVIDER != "openai":
|
||||
@ -205,9 +205,9 @@ def list_available_model_versions(
|
||||
return GPT_4_MODEL_VERSIONS + GPT_3_5_TURBO_MODEL_VERSIONS
|
||||
|
||||
|
||||
@admin_router.get("/utils/default-model")
|
||||
@basic_router.get("/utils/default-model")
|
||||
def get_default_model(
|
||||
_: User | None = Depends(current_admin_user),
|
||||
_: User | None = Depends(current_user),
|
||||
) -> str:
|
||||
# currently only support selecting different models for OpenAI
|
||||
if GEN_AI_MODEL_PROVIDER != "openai":
|
||||
|
@ -7,12 +7,12 @@ from danswer.db.models import StarterMessage
|
||||
from danswer.search.enums import RecencyBiasSetting
|
||||
from danswer.server.features.document_set.models import DocumentSet
|
||||
from danswer.server.features.prompt.models import PromptSnapshot
|
||||
from danswer.server.models import MinimalUserSnapshot
|
||||
|
||||
|
||||
class CreatePersonaRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
shared: bool
|
||||
num_chunks: float
|
||||
llm_relevance_filter: bool
|
||||
is_public: bool
|
||||
@ -29,8 +29,8 @@ class CreatePersonaRequest(BaseModel):
|
||||
|
||||
class PersonaSnapshot(BaseModel):
|
||||
id: int
|
||||
owner: MinimalUserSnapshot | None
|
||||
name: str
|
||||
shared: bool
|
||||
is_visible: bool
|
||||
is_public: bool
|
||||
display_priority: int | None
|
||||
@ -43,6 +43,7 @@ class PersonaSnapshot(BaseModel):
|
||||
default_persona: bool
|
||||
prompts: list[PromptSnapshot]
|
||||
document_sets: list[DocumentSet]
|
||||
users: list[UUID]
|
||||
groups: list[int]
|
||||
|
||||
@classmethod
|
||||
@ -53,7 +54,11 @@ class PersonaSnapshot(BaseModel):
|
||||
return PersonaSnapshot(
|
||||
id=persona.id,
|
||||
name=persona.name,
|
||||
shared=persona.user_id is None,
|
||||
owner=(
|
||||
MinimalUserSnapshot(id=persona.user.id, email=persona.user.email)
|
||||
if persona.user
|
||||
else None
|
||||
),
|
||||
is_visible=persona.is_visible,
|
||||
is_public=persona.is_public,
|
||||
display_priority=persona.display_priority,
|
||||
@ -69,6 +74,7 @@ class PersonaSnapshot(BaseModel):
|
||||
DocumentSet.from_model(document_set_model)
|
||||
for document_set_model in persona.document_sets
|
||||
],
|
||||
users=[user.id for user in persona.users],
|
||||
groups=[user_group.id for user_group in persona.groups],
|
||||
)
|
||||
|
||||
|
@ -4,7 +4,6 @@ from fastapi import HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
from starlette import status
|
||||
|
||||
from danswer.auth.users import current_admin_user
|
||||
from danswer.auth.users import current_user
|
||||
from danswer.db.chat import get_personas_by_ids
|
||||
from danswer.db.chat import get_prompt_by_id
|
||||
@ -32,8 +31,6 @@ def create_update_prompt(
|
||||
user: User | None,
|
||||
db_session: Session,
|
||||
) -> PromptSnapshot:
|
||||
user_id = user.id if user is not None else None
|
||||
|
||||
personas = (
|
||||
list(
|
||||
get_personas_by_ids(
|
||||
@ -47,7 +44,7 @@ def create_update_prompt(
|
||||
|
||||
prompt = upsert_prompt(
|
||||
prompt_id=prompt_id,
|
||||
user_id=user_id,
|
||||
user=user,
|
||||
name=create_prompt_request.name,
|
||||
description=create_prompt_request.description,
|
||||
system_prompt=create_prompt_request.system_prompt,
|
||||
@ -55,7 +52,6 @@ def create_update_prompt(
|
||||
include_citations=create_prompt_request.include_citations,
|
||||
datetime_aware=create_prompt_request.datetime_aware,
|
||||
personas=personas,
|
||||
shared=create_prompt_request.shared,
|
||||
db_session=db_session,
|
||||
)
|
||||
return PromptSnapshot.from_model(prompt)
|
||||
@ -64,7 +60,7 @@ def create_update_prompt(
|
||||
@basic_router.post("")
|
||||
def create_prompt(
|
||||
create_prompt_request: CreatePromptRequest,
|
||||
user: User | None = Depends(current_admin_user),
|
||||
user: User | None = Depends(current_user),
|
||||
db_session: Session = Depends(get_session),
|
||||
) -> PromptSnapshot:
|
||||
try:
|
||||
@ -124,7 +120,7 @@ def delete_prompt(
|
||||
) -> None:
|
||||
mark_prompt_as_deleted(
|
||||
prompt_id=prompt_id,
|
||||
user_id=user.id if user is not None else None,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
|
||||
@ -150,7 +146,7 @@ def get_prompt(
|
||||
return PromptSnapshot.from_model(
|
||||
get_prompt_by_id(
|
||||
prompt_id=prompt_id,
|
||||
user_id=user.id if user is not None else None,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
)
|
||||
|
@ -6,7 +6,6 @@ from danswer.db.models import Prompt
|
||||
class CreatePromptRequest(BaseModel):
|
||||
name: str
|
||||
description: str
|
||||
shared: bool
|
||||
system_prompt: str
|
||||
task_prompt: str
|
||||
include_citations: bool = False
|
||||
@ -17,7 +16,6 @@ class CreatePromptRequest(BaseModel):
|
||||
class PromptSnapshot(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
shared: bool
|
||||
description: str
|
||||
system_prompt: str
|
||||
task_prompt: str
|
||||
@ -34,7 +32,6 @@ class PromptSnapshot(BaseModel):
|
||||
return PromptSnapshot(
|
||||
id=prompt.id,
|
||||
name=prompt.name,
|
||||
shared=prompt.user_id is None,
|
||||
description=prompt.description,
|
||||
system_prompt=prompt.system_prompt,
|
||||
task_prompt=prompt.task_prompt,
|
||||
|
@ -140,7 +140,7 @@ def patch_slack_bot_config(
|
||||
existing_persona_id = existing_slack_bot_config.persona_id
|
||||
if existing_persona_id is not None:
|
||||
persona = get_persona_by_id(
|
||||
persona_id=existing_persona_id, user_id=None, db_session=db_session
|
||||
persona_id=existing_persona_id, user=None, db_session=db_session
|
||||
)
|
||||
|
||||
if not persona.name.startswith(SLACK_BOT_PERSONA_PREFIX):
|
||||
|
@ -1,6 +1,7 @@
|
||||
from typing import Generic
|
||||
from typing import Optional
|
||||
from typing import TypeVar
|
||||
from uuid import UUID
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic.generics import GenericModel
|
||||
@ -21,3 +22,8 @@ class ApiKey(BaseModel):
|
||||
|
||||
class IdReturn(BaseModel):
|
||||
id: int
|
||||
|
||||
|
||||
class MinimalUserSnapshot(BaseModel):
|
||||
id: UUID
|
||||
email: str
|
||||
|
@ -327,7 +327,7 @@ def get_max_document_tokens(
|
||||
try:
|
||||
persona = get_persona_by_id(
|
||||
persona_id=persona_id,
|
||||
user_id=user.id if user else None,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
)
|
||||
except ValueError:
|
||||
|
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { DocumentSet, UserGroup } from "@/lib/types";
|
||||
import { Button, Divider, Text } from "@tremor/react";
|
||||
import { CCPairBasicInfo, DocumentSet, User, UserGroup } from "@/lib/types";
|
||||
import { Button, Divider, Italic, Text } from "@tremor/react";
|
||||
import {
|
||||
ArrayHelpers,
|
||||
ErrorMessage,
|
||||
@ -29,6 +29,8 @@ import { EE_ENABLED } from "@/lib/constants";
|
||||
import { useUserGroups } from "@/lib/hooks";
|
||||
import { Bubble } from "@/components/Bubble";
|
||||
import { GroupsIcon } from "@/components/icons/icons";
|
||||
import { SuccessfulPersonaUpdateRedirectType } from "./enums";
|
||||
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
|
||||
|
||||
function Label({ children }: { children: string | JSX.Element }) {
|
||||
return (
|
||||
@ -40,16 +42,24 @@ function SubLabel({ children }: { children: string | JSX.Element }) {
|
||||
return <div className="text-sm text-subtle mb-2">{children}</div>;
|
||||
}
|
||||
|
||||
export function PersonaEditor({
|
||||
export function AssistantEditor({
|
||||
existingPersona,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
llmOverrideOptions,
|
||||
defaultLLM,
|
||||
user,
|
||||
defaultPublic,
|
||||
redirectType,
|
||||
}: {
|
||||
existingPersona?: Persona | null;
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
documentSets: DocumentSet[];
|
||||
llmOverrideOptions: string[];
|
||||
defaultLLM: string;
|
||||
user: User | null;
|
||||
defaultPublic: boolean;
|
||||
redirectType: SuccessfulPersonaUpdateRedirectType;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -99,7 +109,7 @@ export function PersonaEditor({
|
||||
system_prompt: existingPrompt?.system_prompt ?? "",
|
||||
task_prompt: existingPrompt?.task_prompt ?? "",
|
||||
disable_retrieval: (existingPersona?.num_chunks ?? 10) === 0,
|
||||
is_public: existingPersona?.is_public ?? true,
|
||||
is_public: existingPersona?.is_public ?? defaultPublic,
|
||||
document_set_ids:
|
||||
existingPersona?.document_sets?.map(
|
||||
(documentSet) => documentSet.id
|
||||
@ -116,9 +126,9 @@ export function PersonaEditor({
|
||||
}}
|
||||
validationSchema={Yup.object()
|
||||
.shape({
|
||||
name: Yup.string().required("Must give the Persona a name!"),
|
||||
name: Yup.string().required("Must give the Assistant a name!"),
|
||||
description: Yup.string().required(
|
||||
"Must give the Persona a description!"
|
||||
"Must give the Assistant a description!"
|
||||
),
|
||||
system_prompt: Yup.string(),
|
||||
task_prompt: Yup.string(),
|
||||
@ -187,12 +197,14 @@ export function PersonaEditor({
|
||||
existingPromptId: existingPrompt?.id,
|
||||
...values,
|
||||
num_chunks: numChunks,
|
||||
users: user ? [user.id] : undefined,
|
||||
groups,
|
||||
});
|
||||
} else {
|
||||
[promptResponse, personaResponse] = await createPersona({
|
||||
...values,
|
||||
num_chunks: numChunks,
|
||||
users: user ? [user.id] : undefined,
|
||||
groups,
|
||||
});
|
||||
}
|
||||
@ -201,51 +213,53 @@ export function PersonaEditor({
|
||||
if (!promptResponse.ok) {
|
||||
error = await promptResponse.text();
|
||||
}
|
||||
if (personaResponse && !personaResponse.ok) {
|
||||
if (!personaResponse) {
|
||||
error = "Failed to create Assistant - no response received";
|
||||
} else if (!personaResponse.ok) {
|
||||
error = await personaResponse.text();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
if (error || !personaResponse) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: `Failed to create Persona - ${error}`,
|
||||
message: `Failed to create Assistant - ${error}`,
|
||||
});
|
||||
formikHelpers.setSubmitting(false);
|
||||
} else {
|
||||
router.push(`/admin/personas?u=${Date.now()}`);
|
||||
router.push(
|
||||
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
|
||||
? `/admin/assistants?u=${Date.now()}`
|
||||
: `/chat?assistantId=${
|
||||
((await personaResponse.json()) as Persona).id
|
||||
}`
|
||||
);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, values, setFieldValue }) => (
|
||||
<Form>
|
||||
<div className="pb-6">
|
||||
<HidableSection sectionTitle="Who am I?">
|
||||
<HidableSection sectionTitle="Basics">
|
||||
<>
|
||||
<TextFormField
|
||||
name="name"
|
||||
label="Name"
|
||||
disabled={isUpdate}
|
||||
subtext="Users will be able to select this Persona based on this name."
|
||||
subtext="Users will be able to select this Assistant based on this name."
|
||||
/>
|
||||
|
||||
<TextFormField
|
||||
name="description"
|
||||
label="Description"
|
||||
subtext="Provide a short descriptions which gives users a hint as to what they should use this Persona for."
|
||||
subtext="Provide a short descriptions which gives users a hint as to what they should use this Assistant for."
|
||||
/>
|
||||
</>
|
||||
</HidableSection>
|
||||
|
||||
<Divider />
|
||||
|
||||
<HidableSection sectionTitle="Customize my response style">
|
||||
<>
|
||||
<TextFormField
|
||||
name="system_prompt"
|
||||
label="System Prompt"
|
||||
isTextArea={true}
|
||||
subtext={
|
||||
'Give general info about what the Persona is about. For example, "You are an assistant for On-Call engineers. Your goal is to read the provided context documents and give recommendations as to how to resolve the issue."'
|
||||
'Give general info about what the Assistant is about. For example, "You are an assistant for On-Call engineers. Your goal is to read the provided context documents and give recommendations as to how to resolve the issue."'
|
||||
}
|
||||
onChange={(e) => {
|
||||
setFieldValue("system_prompt", e.target.value);
|
||||
@ -260,11 +274,11 @@ export function PersonaEditor({
|
||||
|
||||
<TextFormField
|
||||
name="task_prompt"
|
||||
label="Task Prompt"
|
||||
label="Task Prompt (Optional)"
|
||||
isTextArea={true}
|
||||
subtext={
|
||||
'Give specific instructions as to what to do with the user query. For example, "Find any relevant sections from the provided documents that can help the user resolve their issue and explain how they are relevant."'
|
||||
}
|
||||
subtext={`Give specific instructions as to what to do with the user query.
|
||||
For example, "Find any relevant sections from the provided documents that can
|
||||
help the user resolve their issue and explain how they are relevant."`}
|
||||
onChange={(e) => {
|
||||
setFieldValue("task_prompt", e.target.value);
|
||||
triggerFinalPromptUpdate(
|
||||
@ -276,35 +290,6 @@ export function PersonaEditor({
|
||||
error={finalPromptError}
|
||||
/>
|
||||
|
||||
{!values.disable_retrieval && (
|
||||
<BooleanFormField
|
||||
name="include_citations"
|
||||
label="Include Citations"
|
||||
subtext={`
|
||||
If set, the response will include bracket citations ([1], [2], etc.)
|
||||
for each document used by the LLM to help inform the response. This is
|
||||
the same technique used by the default Personas. In general, we recommend
|
||||
to leave this enabled in order to increase trust in the LLM answer.`}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BooleanFormField
|
||||
name="disable_retrieval"
|
||||
label="Disable Retrieval"
|
||||
subtext={`
|
||||
If set, the Persona will not fetch any context documents to aid in the response.
|
||||
Instead, it will only use the supplied system and task prompts plus the user
|
||||
query in order to generate a response`}
|
||||
onChange={(e) => {
|
||||
setFieldValue("disable_retrieval", e.target.checked);
|
||||
triggerFinalPromptUpdate(
|
||||
values.system_prompt,
|
||||
values.task_prompt,
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
<Label>Final Prompt</Label>
|
||||
|
||||
{finalPrompt ? (
|
||||
@ -319,73 +304,100 @@ export function PersonaEditor({
|
||||
|
||||
<Divider />
|
||||
|
||||
{!values.disable_retrieval && (
|
||||
{ccPairs.length > 0 && (
|
||||
<>
|
||||
<HidableSection sectionTitle="What data should I have access to?">
|
||||
<HidableSection
|
||||
sectionTitle="[Advanced] Knowledge Base"
|
||||
defaultHidden
|
||||
>
|
||||
<>
|
||||
<FieldArray
|
||||
name="document_set_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<BooleanFormField
|
||||
name="disable_retrieval"
|
||||
label="Disable Retrieval"
|
||||
subtext={`
|
||||
If set, the Assistant will not fetch any context documents to aid in the response.
|
||||
Instead, it will only use the supplied system and task prompts plus the user
|
||||
query in order to generate a response`}
|
||||
onChange={(e) => {
|
||||
setFieldValue("disable_retrieval", e.target.checked);
|
||||
triggerFinalPromptUpdate(
|
||||
values.system_prompt,
|
||||
values.task_prompt,
|
||||
e.target.checked
|
||||
);
|
||||
}}
|
||||
/>
|
||||
|
||||
{!values.disable_retrieval && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<SubLabel>
|
||||
<>
|
||||
Select which{" "}
|
||||
<SubLabel>
|
||||
<>
|
||||
Select which{" "}
|
||||
{!user || user.role === "admin" ? (
|
||||
<Link
|
||||
href="/admin/documents/sets"
|
||||
className="text-blue-500"
|
||||
target="_blank"
|
||||
>
|
||||
Document Sets
|
||||
</Link>{" "}
|
||||
that this Persona should search through. If
|
||||
none are specified, the Persona will search
|
||||
through all available documents in order to
|
||||
try and response to queries.
|
||||
</>
|
||||
</SubLabel>
|
||||
</div>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
||||
{documentSets.map((documentSet) => {
|
||||
const ind = values.document_set_ids.indexOf(
|
||||
documentSet.id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={documentSet.id}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-hover"
|
||||
: " bg-background hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(documentSet.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
{documentSet.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</Link>
|
||||
) : (
|
||||
"Document Sets"
|
||||
)}{" "}
|
||||
that this Assistant should search through. If
|
||||
none are specified, the Assistant will search
|
||||
through all available documents in order to try
|
||||
and respond to queries.
|
||||
</>
|
||||
</SubLabel>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
|
||||
{documentSets.length > 0 ? (
|
||||
<FieldArray
|
||||
name="document_set_ids"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
||||
{documentSets.map((documentSet) => {
|
||||
const ind =
|
||||
values.document_set_ids.indexOf(
|
||||
documentSet.id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<DocumentSetSelectable
|
||||
key={documentSet.id}
|
||||
documentSet={documentSet}
|
||||
isSelected={isSelected}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(documentSet.id);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<Italic className="text-sm">
|
||||
No Document Sets available.{" "}
|
||||
{user?.role !== "admin" && (
|
||||
<>
|
||||
If this functionality would be useful, reach
|
||||
out to the administrators of Danswer for
|
||||
assistance.
|
||||
</>
|
||||
)}
|
||||
</Italic>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
</HidableSection>
|
||||
|
||||
@ -393,73 +405,38 @@ export function PersonaEditor({
|
||||
</>
|
||||
)}
|
||||
|
||||
{EE_ENABLED && userGroups && (
|
||||
{!values.disable_retrieval && (
|
||||
<>
|
||||
<HidableSection sectionTitle="Which groups should have access to this Persona?">
|
||||
<HidableSection
|
||||
sectionTitle="[Advanced] Response Style"
|
||||
defaultHidden
|
||||
>
|
||||
<>
|
||||
<BooleanFormField
|
||||
name="is_public"
|
||||
label="Is Public?"
|
||||
subtext="If set, this Persona will be available to all users. If not, only the specified User Groups will be able to access it."
|
||||
name="include_citations"
|
||||
label="Include Citations"
|
||||
subtext={`
|
||||
If set, the response will include bracket citations ([1], [2], etc.)
|
||||
for each document used by the LLM to help inform the response. This is
|
||||
the same technique used by the default Assistants. In general, we recommend
|
||||
to leave this enabled in order to increase trust in the LLM answer.`}
|
||||
/>
|
||||
|
||||
{userGroups &&
|
||||
userGroups.length > 0 &&
|
||||
!values.is_public && (
|
||||
<div>
|
||||
<Text>
|
||||
Select which User Groups should have access to
|
||||
this Persona.
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{userGroups.map((userGroup) => {
|
||||
const isSelected = values.groups.includes(
|
||||
userGroup.id
|
||||
);
|
||||
return (
|
||||
<Bubble
|
||||
key={userGroup.id}
|
||||
isSelected={isSelected}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setFieldValue(
|
||||
"groups",
|
||||
values.groups.filter(
|
||||
(id) => id !== userGroup.id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setFieldValue("groups", [
|
||||
...values.groups,
|
||||
userGroup.id,
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex">
|
||||
<GroupsIcon />
|
||||
<div className="ml-1">
|
||||
{userGroup.name}
|
||||
</div>
|
||||
</div>
|
||||
</Bubble>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</HidableSection>
|
||||
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{llmOverrideOptions.length > 0 && defaultLLM && (
|
||||
<>
|
||||
<HidableSection sectionTitle="[Advanced] Model Selection">
|
||||
<HidableSection
|
||||
sectionTitle="[Advanced] Model Selection"
|
||||
defaultHidden
|
||||
>
|
||||
<>
|
||||
<Text>
|
||||
Pick which LLM to use for this Persona. If left as
|
||||
Pick which LLM to use for this Assistant. If left as
|
||||
Default, will use <b className="italic">{defaultLLM}</b>
|
||||
.
|
||||
<br />
|
||||
@ -496,7 +473,10 @@ export function PersonaEditor({
|
||||
|
||||
{!values.disable_retrieval && (
|
||||
<>
|
||||
<HidableSection sectionTitle="[Advanced] Retrieval Customization">
|
||||
<HidableSection
|
||||
sectionTitle="[Advanced] Retrieval Customization"
|
||||
defaultHidden
|
||||
>
|
||||
<>
|
||||
<TextFormField
|
||||
name="num_chunks"
|
||||
@ -505,10 +485,7 @@ export function PersonaEditor({
|
||||
<div>
|
||||
How many chunks should we feed into the LLM when
|
||||
generating the final response? Each chunk is ~400
|
||||
words long. If you are using gpt-3.5-turbo or other
|
||||
similar models, setting this to a value greater than
|
||||
5 will result in errors at query time due to the
|
||||
model's input length limit.
|
||||
words long.
|
||||
<br />
|
||||
<br />
|
||||
If unspecified, will use 10 chunks.
|
||||
@ -537,14 +514,17 @@ export function PersonaEditor({
|
||||
</>
|
||||
)}
|
||||
|
||||
<HidableSection sectionTitle="[Advanced] Starter Messages">
|
||||
<HidableSection
|
||||
sectionTitle="[Advanced] Starter Messages"
|
||||
defaultHidden
|
||||
>
|
||||
<>
|
||||
<div className="mb-4">
|
||||
<SubLabel>
|
||||
Starter Messages help guide users to use this Persona.
|
||||
Starter Messages help guide users to use this Assistant.
|
||||
They are shown to the user as clickable options when they
|
||||
select this Persona. When selected, the specified message
|
||||
is sent to the LLM as the initial user message.
|
||||
select this Assistant. When selected, the specified
|
||||
message is sent to the LLM as the initial user message.
|
||||
</SubLabel>
|
||||
</div>
|
||||
|
||||
@ -686,6 +666,67 @@ export function PersonaEditor({
|
||||
|
||||
<Divider />
|
||||
|
||||
{EE_ENABLED && userGroups && (!user || user.role === "admin") && (
|
||||
<>
|
||||
<HidableSection sectionTitle="Access">
|
||||
<>
|
||||
<BooleanFormField
|
||||
name="is_public"
|
||||
label="Is Public?"
|
||||
subtext="If set, this Assistant will be available to all users. If not, only the specified User Groups will be able to access it."
|
||||
/>
|
||||
|
||||
{userGroups &&
|
||||
userGroups.length > 0 &&
|
||||
!values.is_public && (
|
||||
<div>
|
||||
<Text>
|
||||
Select which User Groups should have access to
|
||||
this Assistant.
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-2 mt-2">
|
||||
{userGroups.map((userGroup) => {
|
||||
const isSelected = values.groups.includes(
|
||||
userGroup.id
|
||||
);
|
||||
return (
|
||||
<Bubble
|
||||
key={userGroup.id}
|
||||
isSelected={isSelected}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
setFieldValue(
|
||||
"groups",
|
||||
values.groups.filter(
|
||||
(id) => id !== userGroup.id
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setFieldValue("groups", [
|
||||
...values.groups,
|
||||
userGroup.id,
|
||||
]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="flex">
|
||||
<GroupsIcon />
|
||||
<div className="ml-1">
|
||||
{userGroup.name}
|
||||
</div>
|
||||
</div>
|
||||
</Bubble>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
</HidableSection>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="mx-auto"
|
@ -12,6 +12,18 @@ import { deletePersona, personaComparator } from "./lib";
|
||||
import { FiEdit } from "react-icons/fi";
|
||||
import { TrashIcon } from "@/components/icons/icons";
|
||||
|
||||
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
|
||||
if (persona.default_persona) {
|
||||
return <Text>Built-In</Text>;
|
||||
}
|
||||
|
||||
if (persona.is_public) {
|
||||
return <Text>Global</Text>;
|
||||
}
|
||||
|
||||
return <Text>Personal {persona.owner && <>({persona.owner.email})</>}</Text>;
|
||||
}
|
||||
|
||||
export function PersonasTable({ personas }: { personas: Persona[] }) {
|
||||
const router = useRouter();
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -64,13 +76,13 @@ export function PersonasTable({ personas }: { personas: Persona[] }) {
|
||||
{popup}
|
||||
|
||||
<Text className="my-2">
|
||||
Personas will be displayed as options on the Chat / Search interfaces in
|
||||
the order they are displayed below. Personas marked as hidden will not
|
||||
be displayed.
|
||||
Assistants will be displayed as options on the Chat / Search interfaces
|
||||
in the order they are displayed below. Assistants marked as hidden will
|
||||
not be displayed.
|
||||
</Text>
|
||||
|
||||
<DraggableTable
|
||||
headers={["Name", "Description", "Built-In", "Is Visible", "Delete"]}
|
||||
headers={["Name", "Description", "Type", "Is Visible", "Delete"]}
|
||||
rows={finalPersonaValues.map((persona) => {
|
||||
return {
|
||||
id: persona.id.toString(),
|
||||
@ -81,7 +93,7 @@ export function PersonasTable({ personas }: { personas: Persona[] }) {
|
||||
className="mr-1 my-auto cursor-pointer"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/admin/personas/${persona.id}?u=${Date.now()}`
|
||||
`/admin/assistants/${persona.id}?u=${Date.now()}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
@ -96,7 +108,7 @@ export function PersonasTable({ personas }: { personas: Persona[] }) {
|
||||
>
|
||||
{persona.description}
|
||||
</p>,
|
||||
persona.default_persona ? "Yes" : "No",
|
||||
<PersonaTypeDisplay key={persona.id} persona={persona} />,
|
||||
<div
|
||||
key="is_visible"
|
||||
onClick={async () => {
|
53
web/src/app/admin/assistants/[personaId]/page.tsx
Normal file
53
web/src/app/admin/assistants/[personaId]/page.tsx
Normal file
@ -0,0 +1,53 @@
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { AssistantEditor } from "../AssistantEditor";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Card, Title } from "@tremor/react";
|
||||
import { DeletePersonaButton } from "./DeletePersonaButton";
|
||||
import { fetchPersonaEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
|
||||
import { SuccessfulPersonaUpdateRedirectType } from "../enums";
|
||||
import { RobotIcon } from "@/components/icons/icons";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { personaId: string };
|
||||
}) {
|
||||
const [values, error] = await fetchPersonaEditorInfoSS(params.personaId);
|
||||
|
||||
let body;
|
||||
if (!values) {
|
||||
body = (
|
||||
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<>
|
||||
<Card>
|
||||
<AssistantEditor
|
||||
{...values}
|
||||
defaultPublic={true}
|
||||
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="mt-12">
|
||||
<Title>Delete Assistant</Title>
|
||||
<div className="flex mt-6">
|
||||
<DeletePersonaButton personaId={values.existingPersona!.id} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackButton />
|
||||
|
||||
<AdminPageTitle title="Edit Assistant" icon={<RobotIcon size={32} />} />
|
||||
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
4
web/src/app/admin/assistants/enums.ts
Normal file
4
web/src/app/admin/assistants/enums.ts
Normal file
@ -0,0 +1,4 @@
|
||||
export enum SuccessfulPersonaUpdateRedirectType {
|
||||
ADMIN = "ADMIN",
|
||||
CHAT = "CHAT",
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { DocumentSet, MinimalUserSnapshot } from "@/lib/types";
|
||||
|
||||
export interface StarterMessage {
|
||||
name: string;
|
||||
@ -9,7 +9,6 @@ export interface StarterMessage {
|
||||
export interface Prompt {
|
||||
id: number;
|
||||
name: string;
|
||||
shared: boolean;
|
||||
description: string;
|
||||
system_prompt: string;
|
||||
task_prompt: string;
|
||||
@ -21,7 +20,7 @@ export interface Prompt {
|
||||
export interface Persona {
|
||||
id: number;
|
||||
name: string;
|
||||
shared: boolean;
|
||||
owner: MinimalUserSnapshot | null;
|
||||
is_visible: boolean;
|
||||
is_public: boolean;
|
||||
display_priority: number | null;
|
||||
@ -34,5 +33,6 @@ export interface Persona {
|
||||
llm_model_version_override?: string;
|
||||
starter_messages: StarterMessage[] | null;
|
||||
default_persona: boolean;
|
||||
users: string[];
|
||||
groups: number[];
|
||||
}
|
@ -12,6 +12,7 @@ interface PersonaCreationRequest {
|
||||
llm_relevance_filter: boolean | null;
|
||||
llm_model_version_override: string | null;
|
||||
starter_messages: StarterMessage[] | null;
|
||||
users?: string[];
|
||||
groups: number[];
|
||||
}
|
||||
|
||||
@ -29,6 +30,7 @@ interface PersonaUpdateRequest {
|
||||
llm_relevance_filter: boolean | null;
|
||||
llm_model_version_override: string | null;
|
||||
starter_messages: StarterMessage[] | null;
|
||||
users?: string[];
|
||||
groups: number[];
|
||||
}
|
||||
|
||||
@ -55,7 +57,6 @@ function createPrompt({
|
||||
body: JSON.stringify({
|
||||
name: promptNameFromPersonaName(personaName),
|
||||
description: `Default prompt for persona ${personaName}`,
|
||||
shared: true,
|
||||
system_prompt: systemPrompt,
|
||||
task_prompt: taskPrompt,
|
||||
include_citations: includeCitations,
|
||||
@ -84,7 +85,6 @@ function updatePrompt({
|
||||
body: JSON.stringify({
|
||||
name: promptNameFromPersonaName(personaName),
|
||||
description: `Default prompt for persona ${personaName}`,
|
||||
shared: true,
|
||||
system_prompt: systemPrompt,
|
||||
task_prompt: taskPrompt,
|
||||
include_citations: includeCitations,
|
||||
@ -104,12 +104,12 @@ function buildPersonaAPIBody(
|
||||
llm_relevance_filter,
|
||||
is_public,
|
||||
groups,
|
||||
users,
|
||||
} = creationRequest;
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
shared: true,
|
||||
num_chunks,
|
||||
llm_relevance_filter,
|
||||
llm_filter_extraction: false,
|
||||
@ -119,6 +119,7 @@ function buildPersonaAPIBody(
|
||||
document_set_ids,
|
||||
llm_model_version_override: creationRequest.llm_model_version_override,
|
||||
starter_messages: creationRequest.starter_messages,
|
||||
users,
|
||||
groups,
|
||||
};
|
||||
}
|
||||
@ -139,7 +140,7 @@ export async function createPersona(
|
||||
|
||||
const createPersonaResponse =
|
||||
promptId !== null
|
||||
? await fetch("/api/admin/persona", {
|
||||
? await fetch("/api/persona", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -182,7 +183,7 @@ export async function updatePersona(
|
||||
|
||||
const updatePersonaResponse =
|
||||
promptResponse.ok && promptId
|
||||
? await fetch(`/api/admin/persona/${id}`, {
|
||||
? await fetch(`/api/persona/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
@ -197,7 +198,7 @@ export async function updatePersona(
|
||||
}
|
||||
|
||||
export function deletePersona(personaId: number) {
|
||||
return fetch(`/api/admin/persona/${personaId}`, {
|
||||
return fetch(`/api/persona/${personaId}`, {
|
||||
method: "DELETE",
|
||||
});
|
||||
}
|
42
web/src/app/admin/assistants/new/page.tsx
Normal file
42
web/src/app/admin/assistants/new/page.tsx
Normal file
@ -0,0 +1,42 @@
|
||||
import { AssistantEditor } from "../AssistantEditor";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { RobotIcon } from "@/components/icons/icons";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Card } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { fetchPersonaEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
|
||||
import { SuccessfulPersonaUpdateRedirectType } from "../enums";
|
||||
|
||||
export default async function Page() {
|
||||
const [values, error] = await fetchPersonaEditorInfoSS();
|
||||
|
||||
let body;
|
||||
if (!values) {
|
||||
body = (
|
||||
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<Card>
|
||||
<AssistantEditor
|
||||
{...values}
|
||||
defaultPublic={true}
|
||||
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
|
||||
/>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackButton />
|
||||
|
||||
<AdminPageTitle
|
||||
title="Create a New Persona"
|
||||
icon={<RobotIcon size={32} />}
|
||||
/>
|
||||
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -24,11 +24,11 @@ export default async function Page() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle icon={<RobotIcon size={32} />} title="Personas" />
|
||||
<AdminPageTitle icon={<RobotIcon size={32} />} title="Assistants" />
|
||||
|
||||
<Text className="mb-2">
|
||||
Personas are a way to build custom search/question-answering experiences
|
||||
for different use cases.
|
||||
Assistants are a way to build custom search/question-answering
|
||||
experiences for different use cases.
|
||||
</Text>
|
||||
<Text className="mt-2">They allow you to customize:</Text>
|
||||
<div className="text-sm">
|
||||
@ -43,20 +43,20 @@ export default async function Page() {
|
||||
<div>
|
||||
<Divider />
|
||||
|
||||
<Title>Create a Persona</Title>
|
||||
<Title>Create an Assistant</Title>
|
||||
<Link
|
||||
href="/admin/personas/new"
|
||||
className="flex py-2 px-4 mt-2 border border-border h-fit cursor-pointer hover:bg-hover text-sm w-36"
|
||||
className="flex py-2 px-4 mt-2 border border-border h-fit cursor-pointer hover:bg-hover text-sm w-40"
|
||||
>
|
||||
<div className="mx-auto flex">
|
||||
<FiPlusSquare className="my-auto mr-2" />
|
||||
New Persona
|
||||
New Assistant
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Title>Existing Personas</Title>
|
||||
<Title>Existing Assistants</Title>
|
||||
<PersonasTable personas={personas} />
|
||||
</div>
|
||||
</div>
|
@ -28,7 +28,7 @@ import {
|
||||
Text,
|
||||
} from "@tremor/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Persona } from "../personas/interfaces";
|
||||
import { Persona } from "../assistants/interfaces";
|
||||
import { useState } from "react";
|
||||
import { BookmarkIcon, RobotIcon } from "@/components/icons/icons";
|
||||
|
||||
|
@ -6,7 +6,7 @@ import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||
import { Text } from "@tremor/react";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Persona } from "../../personas/interfaces";
|
||||
import { Persona } from "../../assistants/interfaces";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
|
||||
async function Page({ params }: { params: { id: string } }) {
|
||||
|
@ -3,7 +3,7 @@ import {
|
||||
SlackBotResponseType,
|
||||
SlackBotTokens,
|
||||
} from "@/lib/types";
|
||||
import { Persona } from "../personas/interfaces";
|
||||
import { Persona } from "../assistants/interfaces";
|
||||
|
||||
interface SlackBotConfigCreationRequest {
|
||||
document_sets: number[];
|
||||
|
@ -6,7 +6,7 @@ import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Text } from "@tremor/react";
|
||||
import { Persona } from "../../personas/interfaces";
|
||||
import { Persona } from "../../assistants/interfaces";
|
||||
|
||||
async function Page() {
|
||||
const tasks = [fetchSS("/manage/document-set"), fetchSS("/persona")];
|
||||
|
@ -1,92 +0,0 @@
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { Persona } from "../interfaces";
|
||||
import { PersonaEditor } from "../PersonaEditor";
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Card, Title } from "@tremor/react";
|
||||
import { DeletePersonaButton } from "./DeletePersonaButton";
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: { personaId: string };
|
||||
}) {
|
||||
const [
|
||||
personaResponse,
|
||||
documentSetsResponse,
|
||||
llmOverridesResponse,
|
||||
defaultLLMResponse,
|
||||
] = await Promise.all([
|
||||
fetchSS(`/persona/${params.personaId}`),
|
||||
fetchSS("/manage/document-set"),
|
||||
fetchSS("/admin/persona/utils/list-available-models"),
|
||||
fetchSS("/admin/persona/utils/default-model"),
|
||||
]);
|
||||
|
||||
if (!personaResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch Persona - ${await personaResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!documentSetsResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch document sets - ${await documentSetsResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!llmOverridesResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch LLM override options - ${await documentSetsResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (!defaultLLMResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch default LLM - ${await documentSetsResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
|
||||
const persona = (await personaResponse.json()) as Persona;
|
||||
const llmOverrideOptions = (await llmOverridesResponse.json()) as string[];
|
||||
const defaultLLM = (await defaultLLMResponse.json()) as string;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackButton />
|
||||
<div className="pb-2 mb-4 flex">
|
||||
<h1 className="text-3xl font-bold pl-2">Edit Persona</h1>
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<PersonaEditor
|
||||
existingPersona={persona}
|
||||
documentSets={documentSets}
|
||||
llmOverrideOptions={llmOverrideOptions}
|
||||
defaultLLM={defaultLLM}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<div className="mt-12">
|
||||
<Title>Delete Persona</Title>
|
||||
<div className="flex mt-6">
|
||||
<DeletePersonaButton personaId={persona.id} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,66 +0,0 @@
|
||||
import { PersonaEditor } from "../PersonaEditor";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { RobotIcon } from "@/components/icons/icons";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Card } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
export default async function Page() {
|
||||
const [documentSetsResponse, llmOverridesResponse, defaultLLMResponse] =
|
||||
await Promise.all([
|
||||
fetchSS("/manage/document-set"),
|
||||
fetchSS("/admin/persona/utils/list-available-models"),
|
||||
fetchSS("/admin/persona/utils/default-model"),
|
||||
]);
|
||||
|
||||
if (!documentSetsResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch document sets - ${await documentSetsResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
|
||||
|
||||
if (!llmOverridesResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch LLM override options - ${await documentSetsResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const llmOverrideOptions = (await llmOverridesResponse.json()) as string[];
|
||||
|
||||
if (!defaultLLMResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch default LLM - ${await documentSetsResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const defaultLLM = (await defaultLLMResponse.json()) as string;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BackButton />
|
||||
|
||||
<AdminPageTitle
|
||||
title="Create a New Persona"
|
||||
icon={<RobotIcon size={32} />}
|
||||
/>
|
||||
|
||||
<Card>
|
||||
<PersonaEditor
|
||||
documentSets={documentSets}
|
||||
llmOverrideOptions={llmOverrideOptions}
|
||||
defaultLLM={defaultLLM}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -3,3 +3,19 @@ export interface Settings {
|
||||
search_page_enabled: boolean;
|
||||
default_page: "search" | "chat";
|
||||
}
|
||||
|
||||
export interface EnterpriseSettings {
|
||||
application_name: string | null;
|
||||
use_custom_logo: boolean;
|
||||
|
||||
// custom Chat components
|
||||
custom_header_content: string | null;
|
||||
custom_popup_header: string | null;
|
||||
custom_popup_content: string | null;
|
||||
}
|
||||
|
||||
export interface CombinedSettings {
|
||||
settings: Settings;
|
||||
enterpriseSettings: EnterpriseSettings | null;
|
||||
customAnalyticsScript: string | null;
|
||||
}
|
||||
|
59
web/src/app/assistants/edit/[id]/page.tsx
Normal file
59
web/src/app/assistants/edit/[id]/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { Card } from "@tremor/react";
|
||||
import { HeaderWrapper } from "@/components/header/HeaderWrapper";
|
||||
import { FiChevronLeft } from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { AssistantEditor } from "@/app/admin/assistants/AssistantEditor";
|
||||
import { SuccessfulPersonaUpdateRedirectType } from "@/app/admin/assistants/enums";
|
||||
import { fetchPersonaEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
|
||||
|
||||
export default async function Page({ params }: { params: { id: string } }) {
|
||||
const [values, error] = await fetchPersonaEditorInfoSS(params.id);
|
||||
|
||||
let body;
|
||||
if (!values) {
|
||||
body = (
|
||||
<div className="px-32">
|
||||
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<div className="w-full my-16">
|
||||
<div className="px-32">
|
||||
<div className="mx-auto container">
|
||||
<Card>
|
||||
<AssistantEditor
|
||||
{...values}
|
||||
defaultPublic={false}
|
||||
redirectType={SuccessfulPersonaUpdateRedirectType.CHAT}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderWrapper>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex my-auto">
|
||||
<Link href="/chat">
|
||||
<FiChevronLeft
|
||||
className="mr-1 my-auto p-1 hover:bg-hover rounded cursor-pointer"
|
||||
size={32}
|
||||
/>
|
||||
</Link>
|
||||
<h1 className="flex text-xl text-strong font-bold my-auto">
|
||||
Edit Assistant
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</HeaderWrapper>
|
||||
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
59
web/src/app/assistants/new/page.tsx
Normal file
59
web/src/app/assistants/new/page.tsx
Normal file
@ -0,0 +1,59 @@
|
||||
import { Card } from "@tremor/react";
|
||||
import { HeaderWrapper } from "@/components/header/HeaderWrapper";
|
||||
import { FiChevronLeft } from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
import { AssistantEditor } from "@/app/admin/assistants/AssistantEditor";
|
||||
import { SuccessfulPersonaUpdateRedirectType } from "@/app/admin/assistants/enums";
|
||||
import { fetchPersonaEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
|
||||
export default async function Page() {
|
||||
const [values, error] = await fetchPersonaEditorInfoSS();
|
||||
|
||||
let body;
|
||||
if (!values) {
|
||||
body = (
|
||||
<div className="px-32">
|
||||
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
body = (
|
||||
<div className="w-full my-16">
|
||||
<div className="px-32">
|
||||
<div className="mx-auto container">
|
||||
<Card>
|
||||
<AssistantEditor
|
||||
{...values}
|
||||
defaultPublic={false}
|
||||
redirectType={SuccessfulPersonaUpdateRedirectType.CHAT}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<HeaderWrapper>
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex my-auto">
|
||||
<Link href="/chat">
|
||||
<FiChevronLeft
|
||||
className="mr-1 my-auto p-1 hover:bg-hover rounded cursor-pointer"
|
||||
size={32}
|
||||
/>
|
||||
</Link>
|
||||
<h1 className="flex text-xl text-strong font-bold my-auto">
|
||||
New Assistant
|
||||
</h1>
|
||||
</div>
|
||||
</div>
|
||||
</HeaderWrapper>
|
||||
|
||||
{body}
|
||||
</div>
|
||||
);
|
||||
}
|
@ -1,966 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { FiSend, FiShare2, FiStopCircle } from "react-icons/fi";
|
||||
import { AIMessage, HumanMessage } from "./message/Messages";
|
||||
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces";
|
||||
import {
|
||||
BackendChatSession,
|
||||
BackendMessage,
|
||||
ChatSessionSharedStatus,
|
||||
DocumentsResponse,
|
||||
Message,
|
||||
RetrievalType,
|
||||
StreamingError,
|
||||
} from "./interfaces";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { FeedbackType } from "./types";
|
||||
import {
|
||||
buildChatUrl,
|
||||
createChatSession,
|
||||
getCitedDocumentsFromMessage,
|
||||
getHumanAndAIMessageFromMessageNumber,
|
||||
getLastSuccessfulMessageId,
|
||||
handleAutoScroll,
|
||||
handleChatFeedback,
|
||||
nameChatSession,
|
||||
personaIncludesRetrieval,
|
||||
processRawChatHistory,
|
||||
sendMessage,
|
||||
} from "./lib";
|
||||
import { ThreeDots } from "react-loader-spinner";
|
||||
import { FeedbackModal } from "./modal/FeedbackModal";
|
||||
import { DocumentSidebar } from "./documentSidebar/DocumentSidebar";
|
||||
import { Persona } from "../admin/personas/interfaces";
|
||||
import { ChatPersonaSelector } from "./ChatPersonaSelector";
|
||||
import { useFilters } from "@/lib/hooks";
|
||||
import { DocumentSet, Tag, ValidSources } from "@/lib/types";
|
||||
import { ChatFilters } from "./modifiers/ChatFilters";
|
||||
import { buildFilters } from "@/lib/search/utils";
|
||||
import { SelectedDocuments } from "./modifiers/SelectedDocuments";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { ResizableSection } from "@/components/resizable/ResizableSection";
|
||||
import { DanswerInitializingLoader } from "@/components/DanswerInitializingLoader";
|
||||
import { ChatIntro } from "./ChatIntro";
|
||||
import { HEADER_PADDING } from "@/lib/constants";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { useDocumentSelection } from "./useDocumentSelection";
|
||||
import { StarterMessage } from "./StarterMessage";
|
||||
import { ShareChatSessionModal } from "./modal/ShareChatSessionModal";
|
||||
import { SEARCH_PARAM_NAMES, shouldSubmitOnLoad } from "./searchParams";
|
||||
|
||||
const MAX_INPUT_HEIGHT = 200;
|
||||
|
||||
export const Chat = ({
|
||||
existingChatSessionId,
|
||||
existingChatSessionPersonaId,
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
availablePersonas,
|
||||
availableTags,
|
||||
defaultSelectedPersonaId,
|
||||
documentSidebarInitialWidth,
|
||||
shouldhideBeforeScroll,
|
||||
}: {
|
||||
existingChatSessionId: number | null;
|
||||
existingChatSessionPersonaId: number | undefined;
|
||||
availableSources: ValidSources[];
|
||||
availableDocumentSets: DocumentSet[];
|
||||
availablePersonas: Persona[];
|
||||
availableTags: Tag[];
|
||||
defaultSelectedPersonaId?: number; // what persona to default to
|
||||
documentSidebarInitialWidth?: number;
|
||||
shouldhideBeforeScroll?: boolean;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
// used to track whether or not the initial "submit on load" has been performed
|
||||
// this only applies if `?submit-on-load=true` or `?submit-on-load=1` is in the URL
|
||||
// NOTE: this is required due to React strict mode, where all `useEffect` hooks
|
||||
// are run twice on initial load during development
|
||||
const submitOnLoadPerformed = useRef<boolean>(false);
|
||||
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
// fetch messages for the chat session
|
||||
const [isFetchingChatMessages, setIsFetchingChatMessages] = useState(
|
||||
existingChatSessionId !== null
|
||||
);
|
||||
|
||||
// needed so closures (e.g. onSubmit) can access the current value
|
||||
const urlChatSessionId = useRef<number | null>();
|
||||
// this is triggered every time the user switches which chat
|
||||
// session they are using
|
||||
useEffect(() => {
|
||||
urlChatSessionId.current = existingChatSessionId;
|
||||
|
||||
textareaRef.current?.focus();
|
||||
|
||||
// only clear things if we're going from one chat session to another
|
||||
if (chatSessionId !== null && existingChatSessionId !== chatSessionId) {
|
||||
// de-select documents
|
||||
clearSelectedDocuments();
|
||||
// reset all filters
|
||||
filterManager.setSelectedDocumentSets([]);
|
||||
filterManager.setSelectedSources([]);
|
||||
filterManager.setSelectedTags([]);
|
||||
filterManager.setTimeRange(null);
|
||||
if (isStreaming) {
|
||||
setIsCancelled(true);
|
||||
}
|
||||
}
|
||||
|
||||
setChatSessionId(existingChatSessionId);
|
||||
|
||||
async function initialSessionFetch() {
|
||||
if (existingChatSessionId === null) {
|
||||
setIsFetchingChatMessages(false);
|
||||
if (defaultSelectedPersonaId !== undefined) {
|
||||
setSelectedPersona(
|
||||
availablePersonas.find(
|
||||
(persona) => persona.id === defaultSelectedPersonaId
|
||||
)
|
||||
);
|
||||
} else {
|
||||
setSelectedPersona(undefined);
|
||||
}
|
||||
setMessageHistory([]);
|
||||
setChatSessionSharedStatus(ChatSessionSharedStatus.Private);
|
||||
|
||||
// if we're supposed to submit on initial load, then do that here
|
||||
if (
|
||||
shouldSubmitOnLoad(searchParams) &&
|
||||
!submitOnLoadPerformed.current
|
||||
) {
|
||||
submitOnLoadPerformed.current = true;
|
||||
await onSubmit();
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
setIsFetchingChatMessages(true);
|
||||
const response = await fetch(
|
||||
`/api/chat/get-chat-session/${existingChatSessionId}`
|
||||
);
|
||||
const chatSession = (await response.json()) as BackendChatSession;
|
||||
setSelectedPersona(
|
||||
availablePersonas.find(
|
||||
(persona) => persona.id === chatSession.persona_id
|
||||
)
|
||||
);
|
||||
|
||||
const newMessageHistory = processRawChatHistory(chatSession.messages);
|
||||
setMessageHistory(newMessageHistory);
|
||||
|
||||
const latestMessageId =
|
||||
newMessageHistory[newMessageHistory.length - 1]?.messageId;
|
||||
setSelectedMessageForDocDisplay(
|
||||
latestMessageId !== undefined ? latestMessageId : null
|
||||
);
|
||||
|
||||
setChatSessionSharedStatus(chatSession.shared_status);
|
||||
|
||||
setIsFetchingChatMessages(false);
|
||||
|
||||
// if this is a seeded chat, then kick off the AI message generation
|
||||
if (newMessageHistory.length === 1 && !submitOnLoadPerformed.current) {
|
||||
submitOnLoadPerformed.current = true;
|
||||
const seededMessage = newMessageHistory[0].message;
|
||||
await onSubmit({
|
||||
isSeededChat: true,
|
||||
messageOverride: seededMessage,
|
||||
});
|
||||
// force re-name if the chat session doesn't have one
|
||||
if (!chatSession.description) {
|
||||
await nameChatSession(existingChatSessionId, seededMessage);
|
||||
router.refresh(); // need to refresh to update name on sidebar
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
initialSessionFetch();
|
||||
}, [existingChatSessionId]);
|
||||
|
||||
const [chatSessionId, setChatSessionId] = useState<number | null>(
|
||||
existingChatSessionId
|
||||
);
|
||||
const [message, setMessage] = useState(
|
||||
searchParams.get(SEARCH_PARAM_NAMES.USER_MESSAGE) || ""
|
||||
);
|
||||
const [messageHistory, setMessageHistory] = useState<Message[]>([]);
|
||||
const [isStreaming, setIsStreaming] = useState(false);
|
||||
|
||||
// for document display
|
||||
// NOTE: -1 is a special designation that means the latest AI message
|
||||
const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] =
|
||||
useState<number | null>(null);
|
||||
const { aiMessage } = selectedMessageForDocDisplay
|
||||
? getHumanAndAIMessageFromMessageNumber(
|
||||
messageHistory,
|
||||
selectedMessageForDocDisplay
|
||||
)
|
||||
: { aiMessage: null };
|
||||
|
||||
const [selectedPersona, setSelectedPersona] = useState<Persona | undefined>(
|
||||
existingChatSessionPersonaId !== undefined
|
||||
? availablePersonas.find(
|
||||
(persona) => persona.id === existingChatSessionPersonaId
|
||||
)
|
||||
: defaultSelectedPersonaId !== undefined
|
||||
? availablePersonas.find(
|
||||
(persona) => persona.id === defaultSelectedPersonaId
|
||||
)
|
||||
: undefined
|
||||
);
|
||||
const livePersona = selectedPersona || availablePersonas[0];
|
||||
|
||||
const [chatSessionSharedStatus, setChatSessionSharedStatus] =
|
||||
useState<ChatSessionSharedStatus>(ChatSessionSharedStatus.Private);
|
||||
|
||||
useEffect(() => {
|
||||
if (messageHistory.length === 0 && chatSessionId === null) {
|
||||
setSelectedPersona(
|
||||
availablePersonas.find(
|
||||
(persona) => persona.id === defaultSelectedPersonaId
|
||||
)
|
||||
);
|
||||
}
|
||||
}, [defaultSelectedPersonaId]);
|
||||
|
||||
const [
|
||||
selectedDocuments,
|
||||
toggleDocumentSelection,
|
||||
clearSelectedDocuments,
|
||||
selectedDocumentTokens,
|
||||
] = useDocumentSelection();
|
||||
// just choose a conservative default, this will be updated in the
|
||||
// background on initial load / on persona change
|
||||
const [maxTokens, setMaxTokens] = useState<number>(4096);
|
||||
// fetch # of allowed document tokens for the selected Persona
|
||||
useEffect(() => {
|
||||
async function fetchMaxTokens() {
|
||||
const response = await fetch(
|
||||
`/api/chat/max-selected-document-tokens?persona_id=${livePersona.id}`
|
||||
);
|
||||
if (response.ok) {
|
||||
const maxTokens = (await response.json()).max_tokens as number;
|
||||
setMaxTokens(maxTokens);
|
||||
}
|
||||
}
|
||||
|
||||
fetchMaxTokens();
|
||||
}, [livePersona]);
|
||||
|
||||
const filterManager = useFilters();
|
||||
const [finalAvailableSources, finalAvailableDocumentSets] =
|
||||
computeAvailableFilters({
|
||||
selectedPersona,
|
||||
availableSources,
|
||||
availableDocumentSets,
|
||||
});
|
||||
|
||||
// state for cancelling streaming
|
||||
const [isCancelled, setIsCancelled] = useState(false);
|
||||
const isCancelledRef = useRef(isCancelled);
|
||||
useEffect(() => {
|
||||
isCancelledRef.current = isCancelled;
|
||||
}, [isCancelled]);
|
||||
|
||||
const [currentFeedback, setCurrentFeedback] = useState<
|
||||
[FeedbackType, number] | null
|
||||
>(null);
|
||||
const [sharingModalVisible, setSharingModalVisible] =
|
||||
useState<boolean>(false);
|
||||
|
||||
// auto scroll as message comes out
|
||||
const scrollableDivRef = useRef<HTMLDivElement>(null);
|
||||
const endDivRef = useRef<HTMLDivElement>(null);
|
||||
useEffect(() => {
|
||||
if (isStreaming || !message) {
|
||||
handleAutoScroll(endDivRef, scrollableDivRef);
|
||||
}
|
||||
});
|
||||
|
||||
// scroll to bottom initially
|
||||
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
|
||||
shouldhideBeforeScroll !== true
|
||||
);
|
||||
useEffect(() => {
|
||||
endDivRef.current?.scrollIntoView();
|
||||
setHasPerformedInitialScroll(true);
|
||||
}, [isFetchingChatMessages]);
|
||||
|
||||
// handle re-sizing of the text area
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
useEffect(() => {
|
||||
const textarea = textareaRef.current;
|
||||
if (textarea) {
|
||||
textarea.style.height = "0px";
|
||||
textarea.style.height = `${Math.min(
|
||||
textarea.scrollHeight,
|
||||
MAX_INPUT_HEIGHT
|
||||
)}px`;
|
||||
}
|
||||
}, [message]);
|
||||
|
||||
// used for resizing of the document sidebar
|
||||
const masterFlexboxRef = useRef<HTMLDivElement>(null);
|
||||
const [maxDocumentSidebarWidth, setMaxDocumentSidebarWidth] = useState<
|
||||
number | null
|
||||
>(null);
|
||||
const adjustDocumentSidebarWidth = () => {
|
||||
if (masterFlexboxRef.current && document.documentElement.clientWidth) {
|
||||
// numbers below are based on the actual width the center section for different
|
||||
// screen sizes. `1700` corresponds to the custom "3xl" tailwind breakpoint
|
||||
// NOTE: some buffer is needed to account for scroll bars
|
||||
if (document.documentElement.clientWidth > 1700) {
|
||||
setMaxDocumentSidebarWidth(masterFlexboxRef.current.clientWidth - 950);
|
||||
} else if (document.documentElement.clientWidth > 1420) {
|
||||
setMaxDocumentSidebarWidth(masterFlexboxRef.current.clientWidth - 760);
|
||||
} else {
|
||||
setMaxDocumentSidebarWidth(masterFlexboxRef.current.clientWidth - 660);
|
||||
}
|
||||
}
|
||||
};
|
||||
useEffect(() => {
|
||||
adjustDocumentSidebarWidth(); // Adjust the width on initial render
|
||||
window.addEventListener("resize", adjustDocumentSidebarWidth); // Add resize event listener
|
||||
|
||||
return () => {
|
||||
window.removeEventListener("resize", adjustDocumentSidebarWidth); // Cleanup the event listener
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (!documentSidebarInitialWidth && maxDocumentSidebarWidth) {
|
||||
documentSidebarInitialWidth = Math.min(700, maxDocumentSidebarWidth);
|
||||
}
|
||||
|
||||
const onSubmit = async ({
|
||||
messageIdToResend,
|
||||
messageOverride,
|
||||
queryOverride,
|
||||
forceSearch,
|
||||
isSeededChat,
|
||||
}: {
|
||||
messageIdToResend?: number;
|
||||
messageOverride?: string;
|
||||
queryOverride?: string;
|
||||
forceSearch?: boolean;
|
||||
isSeededChat?: boolean;
|
||||
} = {}) => {
|
||||
let currChatSessionId: number;
|
||||
let isNewSession = chatSessionId === null;
|
||||
const searchParamBasedChatSessionName =
|
||||
searchParams.get(SEARCH_PARAM_NAMES.TITLE) || null;
|
||||
|
||||
if (isNewSession) {
|
||||
currChatSessionId = await createChatSession(
|
||||
livePersona?.id || 0,
|
||||
searchParamBasedChatSessionName
|
||||
);
|
||||
} else {
|
||||
currChatSessionId = chatSessionId as number;
|
||||
}
|
||||
setChatSessionId(currChatSessionId);
|
||||
|
||||
const messageToResend = messageHistory.find(
|
||||
(message) => message.messageId === messageIdToResend
|
||||
);
|
||||
const messageToResendIndex = messageToResend
|
||||
? messageHistory.indexOf(messageToResend)
|
||||
: null;
|
||||
if (!messageToResend && messageIdToResend !== undefined) {
|
||||
setPopup({
|
||||
message:
|
||||
"Failed to re-send message - please refresh the page and try again.",
|
||||
type: "error",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let currMessage = messageToResend ? messageToResend.message : message;
|
||||
if (messageOverride) {
|
||||
currMessage = messageOverride;
|
||||
}
|
||||
const currMessageHistory =
|
||||
messageToResendIndex !== null
|
||||
? messageHistory.slice(0, messageToResendIndex)
|
||||
: messageHistory;
|
||||
setMessageHistory([
|
||||
...currMessageHistory,
|
||||
{
|
||||
messageId: 0,
|
||||
message: currMessage,
|
||||
type: "user",
|
||||
},
|
||||
]);
|
||||
setMessage("");
|
||||
|
||||
setIsStreaming(true);
|
||||
let answer = "";
|
||||
let query: string | null = null;
|
||||
let retrievalType: RetrievalType =
|
||||
selectedDocuments.length > 0
|
||||
? RetrievalType.SelectedDocs
|
||||
: RetrievalType.None;
|
||||
let documents: DanswerDocument[] = selectedDocuments;
|
||||
let error: string | null = null;
|
||||
let finalMessage: BackendMessage | null = null;
|
||||
try {
|
||||
const lastSuccessfulMessageId =
|
||||
getLastSuccessfulMessageId(currMessageHistory);
|
||||
for await (const packetBunch of sendMessage({
|
||||
message: currMessage,
|
||||
parentMessageId: lastSuccessfulMessageId,
|
||||
chatSessionId: currChatSessionId,
|
||||
promptId: livePersona?.prompts[0]?.id || 0,
|
||||
filters: buildFilters(
|
||||
filterManager.selectedSources,
|
||||
filterManager.selectedDocumentSets,
|
||||
filterManager.timeRange,
|
||||
filterManager.selectedTags
|
||||
),
|
||||
selectedDocumentIds: selectedDocuments
|
||||
.filter(
|
||||
(document) =>
|
||||
document.db_doc_id !== undefined && document.db_doc_id !== null
|
||||
)
|
||||
.map((document) => document.db_doc_id as number),
|
||||
queryOverride,
|
||||
forceSearch,
|
||||
modelVersion:
|
||||
searchParams.get(SEARCH_PARAM_NAMES.MODEL_VERSION) || undefined,
|
||||
temperature:
|
||||
parseFloat(searchParams.get(SEARCH_PARAM_NAMES.TEMPERATURE) || "") ||
|
||||
undefined,
|
||||
systemPromptOverride:
|
||||
searchParams.get(SEARCH_PARAM_NAMES.SYSTEM_PROMPT) || undefined,
|
||||
useExistingUserMessage: isSeededChat,
|
||||
})) {
|
||||
for (const packet of packetBunch) {
|
||||
if (Object.hasOwn(packet, "answer_piece")) {
|
||||
answer += (packet as AnswerPiecePacket).answer_piece;
|
||||
} else if (Object.hasOwn(packet, "top_documents")) {
|
||||
documents = (packet as DocumentsResponse).top_documents;
|
||||
query = (packet as DocumentsResponse).rephrased_query;
|
||||
retrievalType = RetrievalType.Search;
|
||||
if (documents && documents.length > 0) {
|
||||
// point to the latest message (we don't know the messageId yet, which is why
|
||||
// we have to use -1)
|
||||
setSelectedMessageForDocDisplay(-1);
|
||||
}
|
||||
} else if (Object.hasOwn(packet, "error")) {
|
||||
error = (packet as StreamingError).error;
|
||||
} else if (Object.hasOwn(packet, "message_id")) {
|
||||
finalMessage = packet as BackendMessage;
|
||||
}
|
||||
}
|
||||
setMessageHistory([
|
||||
...currMessageHistory,
|
||||
{
|
||||
messageId: finalMessage?.parent_message || null,
|
||||
message: currMessage,
|
||||
type: "user",
|
||||
},
|
||||
{
|
||||
messageId: finalMessage?.message_id || null,
|
||||
message: error || answer,
|
||||
type: error ? "error" : "assistant",
|
||||
retrievalType,
|
||||
query: finalMessage?.rephrased_query || query,
|
||||
documents: finalMessage?.context_docs?.top_documents || documents,
|
||||
citations: finalMessage?.citations || {},
|
||||
},
|
||||
]);
|
||||
if (isCancelledRef.current) {
|
||||
setIsCancelled(false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (e: any) {
|
||||
const errorMsg = e.message;
|
||||
setMessageHistory([
|
||||
...currMessageHistory,
|
||||
{
|
||||
messageId: null,
|
||||
message: currMessage,
|
||||
type: "user",
|
||||
},
|
||||
{
|
||||
messageId: null,
|
||||
message: errorMsg,
|
||||
type: "error",
|
||||
},
|
||||
]);
|
||||
}
|
||||
setIsStreaming(false);
|
||||
if (isNewSession) {
|
||||
if (finalMessage) {
|
||||
setSelectedMessageForDocDisplay(finalMessage.message_id);
|
||||
}
|
||||
if (!searchParamBasedChatSessionName) {
|
||||
await nameChatSession(currChatSessionId, currMessage);
|
||||
}
|
||||
|
||||
// NOTE: don't switch pages if the user has navigated away from the chat
|
||||
if (
|
||||
currChatSessionId === urlChatSessionId.current ||
|
||||
urlChatSessionId.current === null
|
||||
) {
|
||||
router.push(buildChatUrl(searchParams, currChatSessionId, null), {
|
||||
scroll: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
if (
|
||||
finalMessage?.context_docs &&
|
||||
finalMessage.context_docs.top_documents.length > 0 &&
|
||||
retrievalType === RetrievalType.Search
|
||||
) {
|
||||
setSelectedMessageForDocDisplay(finalMessage.message_id);
|
||||
}
|
||||
};
|
||||
|
||||
const onFeedback = async (
|
||||
messageId: number,
|
||||
feedbackType: FeedbackType,
|
||||
feedbackDetails: string
|
||||
) => {
|
||||
if (chatSessionId === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
const response = await handleChatFeedback(
|
||||
messageId,
|
||||
feedbackType,
|
||||
feedbackDetails
|
||||
);
|
||||
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: "Thanks for your feedback!",
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
const responseJson = await response.json();
|
||||
const errorMsg = responseJson.detail || responseJson.message;
|
||||
setPopup({
|
||||
message: `Failed to submit feedback - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const retrievalDisabled = !personaIncludesRetrieval(livePersona);
|
||||
|
||||
return (
|
||||
<div className="flex w-full overflow-x-hidden" ref={masterFlexboxRef}>
|
||||
{popup}
|
||||
{currentFeedback && (
|
||||
<FeedbackModal
|
||||
feedbackType={currentFeedback[0]}
|
||||
onClose={() => setCurrentFeedback(null)}
|
||||
onSubmit={(feedbackDetails) => {
|
||||
onFeedback(currentFeedback[1], currentFeedback[0], feedbackDetails);
|
||||
setCurrentFeedback(null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
{sharingModalVisible && chatSessionId !== null && (
|
||||
<ShareChatSessionModal
|
||||
chatSessionId={chatSessionId}
|
||||
existingSharedStatus={chatSessionSharedStatus}
|
||||
onClose={() => setSharingModalVisible(false)}
|
||||
onShare={(shared) =>
|
||||
setChatSessionSharedStatus(
|
||||
shared
|
||||
? ChatSessionSharedStatus.Public
|
||||
: ChatSessionSharedStatus.Private
|
||||
)
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
{documentSidebarInitialWidth !== undefined ? (
|
||||
<>
|
||||
<div
|
||||
className={`w-full sm:relative h-screen ${
|
||||
retrievalDisabled ? "pb-[111px]" : "pb-[140px]"
|
||||
}`}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-full ${HEADER_PADDING} flex flex-col overflow-y-auto overflow-x-hidden relative`}
|
||||
ref={scrollableDivRef}
|
||||
>
|
||||
{livePersona && (
|
||||
<div className="sticky top-0 left-80 z-10 w-full bg-background/90 flex">
|
||||
<div className="ml-2 p-1 rounded mt-2 w-fit">
|
||||
<ChatPersonaSelector
|
||||
personas={availablePersonas}
|
||||
selectedPersonaId={livePersona.id}
|
||||
onPersonaChange={(persona) => {
|
||||
if (persona) {
|
||||
setSelectedPersona(persona);
|
||||
textareaRef.current?.focus();
|
||||
router.push(
|
||||
buildChatUrl(searchParams, null, persona.id)
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{chatSessionId !== null && (
|
||||
<div
|
||||
onClick={() => setSharingModalVisible(true)}
|
||||
className="ml-auto mr-6 my-auto border-border border p-2 rounded cursor-pointer hover:bg-hover-light"
|
||||
>
|
||||
<FiShare2 />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{messageHistory.length === 0 &&
|
||||
!isFetchingChatMessages &&
|
||||
!isStreaming && (
|
||||
<ChatIntro
|
||||
availableSources={finalAvailableSources}
|
||||
availablePersonas={availablePersonas}
|
||||
selectedPersona={selectedPersona}
|
||||
handlePersonaSelect={(persona) => {
|
||||
setSelectedPersona(persona);
|
||||
textareaRef.current?.focus();
|
||||
router.push(buildChatUrl(searchParams, null, persona.id));
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div
|
||||
className={
|
||||
"mt-4 pt-12 sm:pt-0 mx-8" +
|
||||
(hasPerformedInitialScroll ? "" : " invisible")
|
||||
}
|
||||
>
|
||||
{messageHistory.map((message, i) => {
|
||||
if (message.type === "user") {
|
||||
return (
|
||||
<div key={i}>
|
||||
<HumanMessage content={message.message} />
|
||||
</div>
|
||||
);
|
||||
} else if (message.type === "assistant") {
|
||||
const isShowingRetrieved =
|
||||
(selectedMessageForDocDisplay !== null &&
|
||||
selectedMessageForDocDisplay === message.messageId) ||
|
||||
(selectedMessageForDocDisplay === -1 &&
|
||||
i === messageHistory.length - 1);
|
||||
const previousMessage =
|
||||
i !== 0 ? messageHistory[i - 1] : null;
|
||||
return (
|
||||
<div key={i}>
|
||||
<AIMessage
|
||||
messageId={message.messageId}
|
||||
content={message.message}
|
||||
query={messageHistory[i]?.query || undefined}
|
||||
personaName={livePersona.name}
|
||||
citedDocuments={getCitedDocumentsFromMessage(message)}
|
||||
isComplete={
|
||||
i !== messageHistory.length - 1 || !isStreaming
|
||||
}
|
||||
hasDocs={
|
||||
(message.documents &&
|
||||
message.documents.length > 0) === true
|
||||
}
|
||||
handleFeedback={
|
||||
i === messageHistory.length - 1 && isStreaming
|
||||
? undefined
|
||||
: (feedbackType) =>
|
||||
setCurrentFeedback([
|
||||
feedbackType,
|
||||
message.messageId as number,
|
||||
])
|
||||
}
|
||||
handleSearchQueryEdit={
|
||||
i === messageHistory.length - 1 && !isStreaming
|
||||
? (newQuery) => {
|
||||
if (!previousMessage) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Cannot edit query of first message - please refresh the page and try again.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (previousMessage.messageId === null) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Cannot edit query of a pending message - please wait a few seconds and try again.",
|
||||
});
|
||||
return;
|
||||
}
|
||||
onSubmit({
|
||||
messageIdToResend:
|
||||
previousMessage.messageId,
|
||||
queryOverride: newQuery,
|
||||
});
|
||||
}
|
||||
: undefined
|
||||
}
|
||||
isCurrentlyShowingRetrieved={isShowingRetrieved}
|
||||
handleShowRetrieved={(messageNumber) => {
|
||||
if (isShowingRetrieved) {
|
||||
setSelectedMessageForDocDisplay(null);
|
||||
} else {
|
||||
if (messageNumber !== null) {
|
||||
setSelectedMessageForDocDisplay(messageNumber);
|
||||
} else {
|
||||
setSelectedMessageForDocDisplay(-1);
|
||||
}
|
||||
}
|
||||
}}
|
||||
handleForceSearch={() => {
|
||||
if (previousMessage && previousMessage.messageId) {
|
||||
onSubmit({
|
||||
messageIdToResend: previousMessage.messageId,
|
||||
forceSearch: true,
|
||||
});
|
||||
} else {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message:
|
||||
"Failed to force search - please refresh the page and try again.",
|
||||
});
|
||||
}
|
||||
}}
|
||||
retrievalDisabled={retrievalDisabled}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div key={i}>
|
||||
<AIMessage
|
||||
messageId={message.messageId}
|
||||
personaName={livePersona.name}
|
||||
content={
|
||||
<p className="text-red-700 text-sm my-auto">
|
||||
{message.message}
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{isStreaming &&
|
||||
messageHistory.length &&
|
||||
messageHistory[messageHistory.length - 1].type === "user" && (
|
||||
<div key={messageHistory.length}>
|
||||
<AIMessage
|
||||
messageId={null}
|
||||
personaName={livePersona.name}
|
||||
content={
|
||||
<div className="text-sm my-auto">
|
||||
<ThreeDots
|
||||
height="30"
|
||||
width="50"
|
||||
color="#3b82f6"
|
||||
ariaLabel="grid-loading"
|
||||
radius="12.5"
|
||||
wrapperStyle={{}}
|
||||
wrapperClass=""
|
||||
visible={true}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
|
||||
<div className={`min-h-[30px] w-full`}></div>
|
||||
|
||||
{livePersona &&
|
||||
livePersona.starter_messages &&
|
||||
livePersona.starter_messages.length > 0 &&
|
||||
selectedPersona &&
|
||||
messageHistory.length === 0 &&
|
||||
!isFetchingChatMessages && (
|
||||
<div
|
||||
className={`
|
||||
mx-auto
|
||||
px-4
|
||||
w-searchbar-xs
|
||||
2xl:w-searchbar-sm
|
||||
3xl:w-searchbar
|
||||
grid
|
||||
gap-4
|
||||
grid-cols-1
|
||||
grid-rows-1
|
||||
mt-4
|
||||
md:grid-cols-2
|
||||
mb-6`}
|
||||
>
|
||||
{livePersona.starter_messages.map((starterMessage, i) => (
|
||||
<div key={i} className="w-full">
|
||||
<StarterMessage
|
||||
starterMessage={starterMessage}
|
||||
onClick={() =>
|
||||
onSubmit({
|
||||
messageOverride: starterMessage.message,
|
||||
})
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div ref={endDivRef} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="absolute bottom-0 z-10 w-full bg-background border-t border-border">
|
||||
<div className="w-full pb-4 pt-2">
|
||||
{!retrievalDisabled && (
|
||||
<div className="flex">
|
||||
<div className="w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar mx-auto px-4 pt-1 flex">
|
||||
{selectedDocuments.length > 0 ? (
|
||||
<SelectedDocuments
|
||||
selectedDocuments={selectedDocuments}
|
||||
/>
|
||||
) : (
|
||||
<ChatFilters
|
||||
{...filterManager}
|
||||
existingSources={finalAvailableSources}
|
||||
availableDocumentSets={finalAvailableDocumentSets}
|
||||
availableTags={availableTags}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-center py-2 max-w-screen-lg mx-auto mb-2">
|
||||
<div className="w-full shrink relative px-4 w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar mx-auto">
|
||||
<textarea
|
||||
ref={textareaRef}
|
||||
autoFocus
|
||||
className={`
|
||||
opacity-100
|
||||
w-full
|
||||
shrink
|
||||
border
|
||||
border-border
|
||||
rounded-lg
|
||||
outline-none
|
||||
placeholder-gray-400
|
||||
pl-4
|
||||
pr-12
|
||||
py-4
|
||||
overflow-hidden
|
||||
h-14
|
||||
${
|
||||
(textareaRef?.current?.scrollHeight || 0) >
|
||||
MAX_INPUT_HEIGHT
|
||||
? "overflow-y-auto"
|
||||
: ""
|
||||
}
|
||||
whitespace-normal
|
||||
break-word
|
||||
overscroll-contain
|
||||
resize-none
|
||||
`}
|
||||
style={{ scrollbarWidth: "thin" }}
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
placeholder="Ask me anything..."
|
||||
value={message}
|
||||
onChange={(e) => setMessage(e.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!event.shiftKey &&
|
||||
message &&
|
||||
!isStreaming
|
||||
) {
|
||||
onSubmit();
|
||||
event.preventDefault();
|
||||
}
|
||||
}}
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
<div className="absolute bottom-4 right-10">
|
||||
<div
|
||||
className={"cursor-pointer"}
|
||||
onClick={() => {
|
||||
if (!isStreaming) {
|
||||
if (message) {
|
||||
onSubmit();
|
||||
}
|
||||
} else {
|
||||
setIsCancelled(true);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{isStreaming ? (
|
||||
<FiStopCircle
|
||||
size={18}
|
||||
className={
|
||||
"text-emphasis w-9 h-9 p-2 rounded-lg hover:bg-hover"
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
<FiSend
|
||||
size={18}
|
||||
className={
|
||||
"text-emphasis w-9 h-9 p-2 rounded-lg " +
|
||||
(message ? "bg-blue-200" : "")
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!retrievalDisabled ? (
|
||||
<ResizableSection
|
||||
intialWidth={documentSidebarInitialWidth}
|
||||
minWidth={400}
|
||||
maxWidth={maxDocumentSidebarWidth || undefined}
|
||||
>
|
||||
<DocumentSidebar
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={toggleDocumentSelection}
|
||||
clearSelectedDocuments={clearSelectedDocuments}
|
||||
selectedDocumentTokens={selectedDocumentTokens}
|
||||
maxTokens={maxTokens}
|
||||
isLoading={isFetchingChatMessages}
|
||||
/>
|
||||
</ResizableSection>
|
||||
) : // Another option is to use a div with the width set to the initial width, so that the
|
||||
// chat section appears in the same place as before
|
||||
// <div style={documentSidebarInitialWidth ? {width: documentSidebarInitialWidth} : {}}></div>
|
||||
null}
|
||||
</>
|
||||
) : (
|
||||
<div className="mx-auto h-full flex flex-col">
|
||||
<div className="my-auto">
|
||||
<DanswerInitializingLoader />
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
@ -1,7 +1,7 @@
|
||||
import { getSourceMetadataForSources, listSourceMetadata } from "@/lib/sources";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import Image from "next/image";
|
||||
import { Persona } from "../admin/personas/interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { Divider } from "@tremor/react";
|
||||
import { FiBookmark, FiCpu, FiInfo, FiX, FiZoomIn } from "react-icons/fi";
|
||||
import { HoverPopup } from "@/components/HoverPopup";
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -1,4 +1,4 @@
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { FiCheck, FiChevronDown } from "react-icons/fi";
|
||||
import { CustomDropdown } from "@/components/Dropdown";
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { StarterMessage } from "../admin/personas/interfaces";
|
||||
import { StarterMessage } from "../admin/assistants/interfaces";
|
||||
|
||||
export function StarterMessage({
|
||||
starterMessage,
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
RetrievalType,
|
||||
StreamingError,
|
||||
} from "./interfaces";
|
||||
import { Persona } from "../admin/personas/interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { ReadonlyURLSearchParams } from "next/navigation";
|
||||
import { SEARCH_PARAM_NAMES } from "./searchParams";
|
||||
|
||||
@ -402,7 +402,7 @@ export function buildChatUrl(
|
||||
if (chatSessionId) {
|
||||
finalSearchParams.push(`${SEARCH_PARAM_NAMES.CHAT_ID}=${chatSessionId}`);
|
||||
}
|
||||
if (personaId) {
|
||||
if (personaId !== null) {
|
||||
finalSearchParams.push(`${SEARCH_PARAM_NAMES.PERSONA_ID}=${personaId}`);
|
||||
}
|
||||
|
||||
|
@ -14,7 +14,7 @@ import {
|
||||
} from "@/lib/types";
|
||||
import { ChatSession } from "./interfaces";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { Persona } from "../admin/personas/interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import {
|
||||
WelcomeModal,
|
||||
@ -23,12 +23,12 @@ import {
|
||||
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
|
||||
import { cookies } from "next/headers";
|
||||
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/contants";
|
||||
import { personaComparator } from "../admin/personas/lib";
|
||||
import { personaComparator } from "../admin/assistants/lib";
|
||||
import { ChatLayout } from "./ChatPage";
|
||||
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
|
||||
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
||||
import { getSettingsSS } from "@/lib/settings";
|
||||
import { Settings } from "../admin/settings/interfaces";
|
||||
import { SIDEBAR_TAB_COOKIE, Tabs } from "./sessionSidebar/constants";
|
||||
|
||||
export default async function Page({
|
||||
searchParams,
|
||||
@ -45,7 +45,6 @@ export default async function Page({
|
||||
fetchSS("/persona?include_default=true"),
|
||||
fetchSS("/chat/get-user-chat-sessions"),
|
||||
fetchSS("/query/valid-tags"),
|
||||
getSettingsSS(),
|
||||
];
|
||||
|
||||
// catch cases where the backend is completely unreachable here
|
||||
@ -58,7 +57,7 @@ export default async function Page({
|
||||
| FullEmbeddingModelResponse
|
||||
| Settings
|
||||
| null
|
||||
)[] = [null, null, null, null, null, null, null, null, null, null];
|
||||
)[] = [null, null, null, null, null, null, null, null, null];
|
||||
try {
|
||||
results = await Promise.all(tasks);
|
||||
} catch (e) {
|
||||
@ -71,7 +70,6 @@ export default async function Page({
|
||||
const personasResponse = results[4] as Response | null;
|
||||
const chatSessionsResponse = results[5] as Response | null;
|
||||
const tagsResponse = results[6] as Response | null;
|
||||
const settings = results[7] as Settings | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
if (!authDisabled && !user) {
|
||||
@ -82,10 +80,6 @@ export default async function Page({
|
||||
return redirect("/auth/waiting-on-verification");
|
||||
}
|
||||
|
||||
if (settings && !settings.chat_page_enabled) {
|
||||
return redirect("/search");
|
||||
}
|
||||
|
||||
let ccPairs: CCPairBasicInfo[] = [];
|
||||
if (ccPairsResponse?.ok) {
|
||||
ccPairs = await ccPairsResponse.json();
|
||||
@ -137,7 +131,7 @@ export default async function Page({
|
||||
console.log(`Failed to fetch tags - ${tagsResponse?.status}`);
|
||||
}
|
||||
|
||||
const defaultPersonaIdRaw = searchParams["personaId"];
|
||||
const defaultPersonaIdRaw = searchParams["assistantId"];
|
||||
const defaultPersonaId = defaultPersonaIdRaw
|
||||
? parseInt(defaultPersonaIdRaw)
|
||||
: undefined;
|
||||
@ -149,6 +143,10 @@ export default async function Page({
|
||||
? parseInt(documentSidebarCookieInitialWidth.value)
|
||||
: undefined;
|
||||
|
||||
const defaultSidebarTab = cookies().get(SIDEBAR_TAB_COOKIE)?.value as
|
||||
| Tabs
|
||||
| undefined;
|
||||
|
||||
const hasAnyConnectors = ccPairs.length > 0;
|
||||
const shouldShowWelcomeModal =
|
||||
!hasCompletedWelcomeFlowSS() &&
|
||||
@ -181,7 +179,6 @@ export default async function Page({
|
||||
|
||||
<ChatLayout
|
||||
user={user}
|
||||
settings={settings}
|
||||
chatSessions={chatSessions}
|
||||
availableSources={availableSources}
|
||||
availableDocumentSets={documentSets}
|
||||
@ -189,6 +186,7 @@ export default async function Page({
|
||||
availableTags={tags}
|
||||
defaultSelectedPersonaId={defaultPersonaId}
|
||||
documentSidebarInitialWidth={finalDocumentSidebarInitialWidth}
|
||||
defaultSidebarTab={defaultSidebarTab}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
@ -3,7 +3,7 @@ import { ReadonlyURLSearchParams } from "next/navigation";
|
||||
// search params
|
||||
export const SEARCH_PARAM_NAMES = {
|
||||
CHAT_ID: "chatId",
|
||||
PERSONA_ID: "personaId",
|
||||
PERSONA_ID: "assistantId",
|
||||
// overrides
|
||||
TEMPERATURE: "temperature",
|
||||
MODEL_VERSION: "model-version",
|
||||
|
105
web/src/app/chat/sessionSidebar/AssistantsTab.tsx
Normal file
105
web/src/app/chat/sessionSidebar/AssistantsTab.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { BasicSelectable } from "@/components/BasicClickable";
|
||||
import { User } from "@/lib/types";
|
||||
import { Text } from "@tremor/react";
|
||||
import Link from "next/link";
|
||||
import { FiEdit } from "react-icons/fi";
|
||||
|
||||
function AssistantDisplay({
|
||||
persona,
|
||||
onSelect,
|
||||
user,
|
||||
}: {
|
||||
persona: Persona;
|
||||
onSelect: (persona: Persona) => void;
|
||||
user: User | null;
|
||||
}) {
|
||||
const isEditable =
|
||||
(!user || user.id === persona.owner?.id) &&
|
||||
!persona.default_persona &&
|
||||
(!persona.is_public || !user || user.role === "admin");
|
||||
|
||||
return (
|
||||
<div className="flex">
|
||||
<div className="w-full" onClick={() => onSelect(persona)}>
|
||||
<BasicSelectable selected={false} fullWidth>
|
||||
<div className="flex">
|
||||
<div className="truncate w-48 3xl:w-56">{persona.name}</div>
|
||||
</div>
|
||||
</BasicSelectable>
|
||||
</div>
|
||||
{isEditable && (
|
||||
<div className="pl-2 my-auto">
|
||||
<Link href={`/assistants/edit/${persona.id}`}>
|
||||
<FiEdit
|
||||
className="my-auto ml-auto hover:bg-hover p-0.5"
|
||||
size={20}
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function AssistantsTab({
|
||||
personas,
|
||||
onPersonaChange,
|
||||
user,
|
||||
}: {
|
||||
personas: Persona[];
|
||||
onPersonaChange: (persona: Persona | null) => void;
|
||||
user: User | null;
|
||||
}) {
|
||||
const globalAssistants = personas.filter((persona) => persona.is_public);
|
||||
const personalAssistants = personas.filter(
|
||||
(persona) =>
|
||||
!user || (persona.users.includes(user.id) && !persona.is_public)
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mt-4 pb-1 overflow-y-auto h-full flex flex-col gap-y-1">
|
||||
<Text className="mx-3 text-xs mb-4">
|
||||
Select an Assistant below to begin a new chat with them!
|
||||
</Text>
|
||||
|
||||
<div className="mx-3">
|
||||
{globalAssistants.length > 0 && (
|
||||
<>
|
||||
<div className="text-xs text-subtle flex pb-0.5 ml-1 mb-1.5 font-bold">
|
||||
Global
|
||||
</div>
|
||||
{globalAssistants.map((persona) => {
|
||||
return (
|
||||
<AssistantDisplay
|
||||
key={persona.id}
|
||||
persona={persona}
|
||||
onSelect={onPersonaChange}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
|
||||
{personalAssistants.length > 0 && (
|
||||
<>
|
||||
<div className="text-xs text-subtle flex pb-0.5 ml-1 mb-1.5 mt-5 font-bold">
|
||||
Personal
|
||||
</div>
|
||||
{personalAssistants.map((persona) => {
|
||||
return (
|
||||
<AssistantDisplay
|
||||
key={persona.id}
|
||||
persona={persona}
|
||||
onSelect={onPersonaChange}
|
||||
user={user}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -21,21 +21,50 @@ import {
|
||||
HEADER_PADDING,
|
||||
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
|
||||
} from "@/lib/constants";
|
||||
|
||||
interface ChatSidebarProps {
|
||||
existingChats: ChatSession[];
|
||||
currentChatSession: ChatSession | null | undefined;
|
||||
user: User | null;
|
||||
}
|
||||
import { ChatTab } from "./ChatTab";
|
||||
import { AssistantsTab } from "./AssistantsTab";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import Cookies from "js-cookie";
|
||||
import { SIDEBAR_TAB_COOKIE, Tabs } from "./constants";
|
||||
|
||||
export const ChatSidebar = ({
|
||||
existingChats,
|
||||
currentChatSession,
|
||||
personas,
|
||||
onPersonaChange,
|
||||
user,
|
||||
}: ChatSidebarProps) => {
|
||||
defaultTab,
|
||||
}: {
|
||||
existingChats: ChatSession[];
|
||||
currentChatSession: ChatSession | null | undefined;
|
||||
personas: Persona[];
|
||||
onPersonaChange: (persona: Persona | null) => void;
|
||||
user: User | null;
|
||||
defaultTab?: Tabs;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
const groupedChatSessions = groupSessionsByDateRange(existingChats);
|
||||
const [openTab, _setOpenTab] = useState(defaultTab || Tabs.CHATS);
|
||||
const setOpenTab = (tab: Tabs) => {
|
||||
Cookies.set(SIDEBAR_TAB_COOKIE, tab);
|
||||
_setOpenTab(tab);
|
||||
};
|
||||
|
||||
function TabOption({ tab }: { tab: Tabs }) {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
"font-bold p-1 rounded-lg hover:bg-hover cursor-pointer " +
|
||||
(openTab === tab ? "bg-hover" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
setOpenTab(tab);
|
||||
}}
|
||||
>
|
||||
{tab}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const [userInfoVisible, setUserInfoVisible] = useState(false);
|
||||
const userInfoRef = useRef<HTMLDivElement>(null);
|
||||
@ -78,8 +107,9 @@ export const ChatSidebar = ({
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex-none
|
||||
w-64
|
||||
2xl:w-72
|
||||
3xl:w-72
|
||||
${HEADER_PADDING}
|
||||
border-r
|
||||
border-border
|
||||
@ -89,49 +119,53 @@ export const ChatSidebar = ({
|
||||
transition-transform`}
|
||||
id="chat-sidebar"
|
||||
>
|
||||
<Link
|
||||
href={
|
||||
"/chat" +
|
||||
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && currentChatSession
|
||||
? `?personaId=${currentChatSession.persona_id}`
|
||||
: "")
|
||||
}
|
||||
className="mx-3 mt-5"
|
||||
>
|
||||
<BasicClickable fullWidth>
|
||||
<div className="flex text-sm">
|
||||
<FiPlusSquare className="my-auto mr-2" /> New Chat
|
||||
</div>
|
||||
</BasicClickable>
|
||||
</Link>
|
||||
|
||||
<div className="mt-1 pb-1 mb-1 ml-3 overflow-y-auto h-full">
|
||||
{Object.entries(groupedChatSessions).map(
|
||||
([dateRange, chatSessions]) => {
|
||||
if (chatSessions.length > 0) {
|
||||
return (
|
||||
<div key={dateRange}>
|
||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-5 font-bold">
|
||||
{dateRange}
|
||||
</div>
|
||||
{chatSessions.map((chat) => {
|
||||
const isSelected = currentChatId === chat.id;
|
||||
return (
|
||||
<div key={`${chat.id}-${chat.name}`} className="mr-3">
|
||||
<ChatSessionDisplay
|
||||
chatSession={chat}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
)}
|
||||
<div className="flex w-full mx-4 mt-4 text-sm gap-x-4 pb-2 border-b border-border">
|
||||
<TabOption tab={Tabs.CHATS} />
|
||||
<TabOption tab={Tabs.ASSISTANTS} />
|
||||
</div>
|
||||
|
||||
{openTab == Tabs.CHATS && (
|
||||
<>
|
||||
<Link
|
||||
href={
|
||||
"/chat" +
|
||||
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&
|
||||
currentChatSession
|
||||
? `?assistantId=${currentChatSession.persona_id}`
|
||||
: "")
|
||||
}
|
||||
className="mx-3 mt-5"
|
||||
>
|
||||
<BasicClickable fullWidth>
|
||||
<div className="flex text-sm">
|
||||
<FiPlusSquare className="my-auto mr-2" /> New Chat
|
||||
</div>
|
||||
</BasicClickable>
|
||||
</Link>
|
||||
<ChatTab
|
||||
existingChats={existingChats}
|
||||
currentChatId={currentChatId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{openTab == Tabs.ASSISTANTS && (
|
||||
<>
|
||||
<Link href="/assistants/new" className="mx-3 mt-5">
|
||||
<BasicClickable fullWidth>
|
||||
<div className="flex text-sm">
|
||||
<FiPlusSquare className="my-auto mr-2" /> New Assistant
|
||||
</div>
|
||||
</BasicClickable>
|
||||
</Link>
|
||||
<AssistantsTab
|
||||
personas={personas}
|
||||
onPersonaChange={onPersonaChange}
|
||||
user={user}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div
|
||||
className="mt-auto py-2 border-t border-border px-3"
|
||||
ref={userInfoRef}
|
||||
|
40
web/src/app/chat/sessionSidebar/ChatTab.tsx
Normal file
40
web/src/app/chat/sessionSidebar/ChatTab.tsx
Normal file
@ -0,0 +1,40 @@
|
||||
import { ChatSession } from "../interfaces";
|
||||
import { groupSessionsByDateRange } from "../lib";
|
||||
import { ChatSessionDisplay } from "./SessionDisplay";
|
||||
|
||||
export function ChatTab({
|
||||
existingChats,
|
||||
currentChatId,
|
||||
}: {
|
||||
existingChats: ChatSession[];
|
||||
currentChatId?: number;
|
||||
}) {
|
||||
const groupedChatSessions = groupSessionsByDateRange(existingChats);
|
||||
|
||||
return (
|
||||
<div className="mt-1 pb-1 mb-1 ml-3 overflow-y-auto h-full">
|
||||
{Object.entries(groupedChatSessions).map(([dateRange, chatSessions]) => {
|
||||
if (chatSessions.length > 0) {
|
||||
return (
|
||||
<div key={dateRange}>
|
||||
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-5 font-bold">
|
||||
{dateRange}
|
||||
</div>
|
||||
{chatSessions.map((chat) => {
|
||||
const isSelected = currentChatId === chat.id;
|
||||
return (
|
||||
<div key={`${chat.id}-${chat.name}`} className="mr-3">
|
||||
<ChatSessionDisplay
|
||||
chatSession={chat}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
6
web/src/app/chat/sessionSidebar/constants.ts
Normal file
6
web/src/app/chat/sessionSidebar/constants.ts
Normal file
@ -0,0 +1,6 @@
|
||||
export const SIDEBAR_TAB_COOKIE = "chatSidebarTab";
|
||||
|
||||
export enum Tabs {
|
||||
CHATS = "Chats",
|
||||
ASSISTANTS = "Assistants",
|
||||
}
|
@ -7,10 +7,8 @@ import {
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
import { redirect } from "next/navigation";
|
||||
import { BackendChatSession } from "../../interfaces";
|
||||
import { Header } from "@/components/Header";
|
||||
import { Header } from "@/components/header/Header";
|
||||
import { SharedChatDisplay } from "./SharedChatDisplay";
|
||||
import { getSettingsSS } from "@/lib/settings";
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
|
||||
async function getSharedChat(chatId: string) {
|
||||
const response = await fetchSS(
|
||||
@ -27,13 +25,12 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
||||
getAuthTypeMetadataSS(),
|
||||
getCurrentUserSS(),
|
||||
getSharedChat(params.chatId),
|
||||
getSettingsSS(),
|
||||
];
|
||||
|
||||
// catch cases where the backend is completely unreachable here
|
||||
// without try / catch, will just raise an exception and the page
|
||||
// will not render
|
||||
let results: (User | AuthTypeMetadata | null)[] = [null, null, null, null];
|
||||
let results: (User | AuthTypeMetadata | null)[] = [null, null, null];
|
||||
try {
|
||||
results = await Promise.all(tasks);
|
||||
} catch (e) {
|
||||
@ -42,7 +39,6 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
||||
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
||||
const user = results[1] as User | null;
|
||||
const chatSession = results[2] as BackendChatSession | null;
|
||||
const settings = results[3] as Settings | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
if (!authDisabled && !user) {
|
||||
@ -56,7 +52,7 @@ export default async function Page({ params }: { params: { chatId: string } }) {
|
||||
return (
|
||||
<div>
|
||||
<div className="absolute top-0 z-40 w-full">
|
||||
<Header user={user} settings={settings} />
|
||||
<Header user={user} />
|
||||
</div>
|
||||
|
||||
<div className="flex relative bg-background text-default overflow-hidden pt-16 h-screen">
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { fetchSettingsSS } from "@/components/settings/lib";
|
||||
import "./globals.css";
|
||||
|
||||
import { Inter } from "next/font/google";
|
||||
import { SettingsProvider } from "@/components/settings/SettingsProvider";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@ -19,12 +21,19 @@ export default async function RootLayout({
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const combinedSettings = await fetchSettingsSS();
|
||||
|
||||
return (
|
||||
<html lang="en">
|
||||
<body
|
||||
className={`${inter.variable} font-sans text-default bg-background`}
|
||||
className={`${inter.variable} font-sans text-default bg-background ${
|
||||
// TODO: remove this once proper dark mode exists
|
||||
process.env.THEME_IS_DARK?.toLowerCase() === "true" ? "dark" : ""
|
||||
}`}
|
||||
>
|
||||
{children}
|
||||
<SettingsProvider settings={combinedSettings}>
|
||||
{children}
|
||||
</SettingsProvider>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
|
@ -1,14 +1,14 @@
|
||||
import { getSettingsSS } from "@/lib/settings";
|
||||
import { fetchSettingsSS } from "@/components/settings/lib";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export default async function Page() {
|
||||
const settings = await getSettingsSS();
|
||||
const settings = await fetchSettingsSS();
|
||||
|
||||
if (!settings) {
|
||||
redirect("/search");
|
||||
}
|
||||
|
||||
if (settings.default_page === "search") {
|
||||
if (settings.settings.default_page === "search") {
|
||||
redirect("/search");
|
||||
} else {
|
||||
redirect("/chat");
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { SearchSection } from "@/components/search/SearchSection";
|
||||
import { Header } from "@/components/Header";
|
||||
import { Header } from "@/components/header/Header";
|
||||
import {
|
||||
AuthTypeMetadata,
|
||||
getAuthTypeMetadataSS,
|
||||
@ -12,19 +12,17 @@ import { fetchSS } from "@/lib/utilsSS";
|
||||
import { CCPairBasicInfo, DocumentSet, Tag, User } from "@/lib/types";
|
||||
import { cookies } from "next/headers";
|
||||
import { SearchType } from "@/lib/search/interfaces";
|
||||
import { Persona } from "../admin/personas/interfaces";
|
||||
import { Persona } from "../admin/assistants/interfaces";
|
||||
import {
|
||||
WelcomeModal,
|
||||
hasCompletedWelcomeFlowSS,
|
||||
} from "@/components/initialSetup/welcome/WelcomeModalWrapper";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { personaComparator } from "../admin/personas/lib";
|
||||
import { personaComparator } from "../admin/assistants/lib";
|
||||
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
|
||||
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
|
||||
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
|
||||
import { getSettingsSS } from "@/lib/settings";
|
||||
import { Settings } from "../admin/settings/interfaces";
|
||||
|
||||
export default async function Home() {
|
||||
// Disable caching so we always get the up to date connector / document set / persona info
|
||||
@ -40,7 +38,6 @@ export default async function Home() {
|
||||
fetchSS("/persona"),
|
||||
fetchSS("/query/valid-tags"),
|
||||
fetchSS("/secondary-index/get-embedding-models"),
|
||||
getSettingsSS(),
|
||||
];
|
||||
|
||||
// catch cases where the backend is completely unreachable here
|
||||
@ -51,9 +48,8 @@ export default async function Home() {
|
||||
| Response
|
||||
| AuthTypeMetadata
|
||||
| FullEmbeddingModelResponse
|
||||
| Settings
|
||||
| null
|
||||
)[] = [null, null, null, null, null, null, null];
|
||||
)[] = [null, null, null, null, null, null];
|
||||
try {
|
||||
results = await Promise.all(tasks);
|
||||
} catch (e) {
|
||||
@ -66,7 +62,6 @@ export default async function Home() {
|
||||
const personaResponse = results[4] as Response | null;
|
||||
const tagsResponse = results[5] as Response | null;
|
||||
const embeddingModelResponse = results[6] as Response | null;
|
||||
const settings = results[7] as Settings | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
if (!authDisabled && !user) {
|
||||
@ -77,10 +72,6 @@ export default async function Home() {
|
||||
return redirect("/auth/waiting-on-verification");
|
||||
}
|
||||
|
||||
if (settings && !settings.search_page_enabled) {
|
||||
return redirect("/chat");
|
||||
}
|
||||
|
||||
let ccPairs: CCPairBasicInfo[] = [];
|
||||
if (ccPairsResponse?.ok) {
|
||||
ccPairs = await ccPairsResponse.json();
|
||||
@ -152,7 +143,7 @@ export default async function Home() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Header user={user} settings={settings} />
|
||||
<Header user={user} />
|
||||
<div className="m-3">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
@ -1,5 +1,4 @@
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { Header } from "@/components/Header";
|
||||
import { Header } from "@/components/header/Header";
|
||||
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
|
||||
import {
|
||||
NotebookIcon,
|
||||
@ -13,7 +12,6 @@ import {
|
||||
ConnectorIcon,
|
||||
SlackIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import { getSettingsSS } from "@/lib/settings";
|
||||
import { User } from "@/lib/types";
|
||||
import {
|
||||
AuthTypeMetadata,
|
||||
@ -30,12 +28,12 @@ import {
|
||||
} from "react-icons/fi";
|
||||
|
||||
export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS(), getSettingsSS()];
|
||||
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()];
|
||||
|
||||
// catch cases where the backend is completely unreachable here
|
||||
// without try / catch, will just raise an exception and the page
|
||||
// will not render
|
||||
let results: (User | AuthTypeMetadata | Settings | null)[] = [null, null];
|
||||
let results: (User | AuthTypeMetadata | null)[] = [null, null];
|
||||
try {
|
||||
results = await Promise.all(tasks);
|
||||
} catch (e) {
|
||||
@ -44,7 +42,6 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
||||
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
|
||||
const user = results[1] as User | null;
|
||||
const settings = results[2] as Settings | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
const requiresVerification = authTypeMetadata?.requiresVerification;
|
||||
@ -63,7 +60,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="h-screen overflow-y-hidden">
|
||||
<div className="absolute top-0 z-50 w-full">
|
||||
<Header user={user} settings={settings} />
|
||||
<Header user={user} />
|
||||
</div>
|
||||
<div className="flex h-full pt-16">
|
||||
<div className="w-80 pt-12 pb-8 h-full border-r border-border">
|
||||
@ -131,10 +128,10 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
name: (
|
||||
<div className="flex">
|
||||
<RobotIcon size={18} />
|
||||
<div className="ml-1">Personas</div>
|
||||
<div className="ml-1">Assistants</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/personas",
|
||||
link: "/admin/assistants",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
|
54
web/src/components/documentSet/DocumentSetSelectable.tsx
Normal file
54
web/src/components/documentSet/DocumentSetSelectable.tsx
Normal file
@ -0,0 +1,54 @@
|
||||
import { DocumentSet, ValidSources } from "@/lib/types";
|
||||
import { CustomCheckbox } from "../CustomCheckbox";
|
||||
import { SourceIcon } from "../SourceIcon";
|
||||
|
||||
export function DocumentSetSelectable({
|
||||
documentSet,
|
||||
isSelected,
|
||||
onSelect,
|
||||
}: {
|
||||
documentSet: DocumentSet;
|
||||
isSelected: boolean;
|
||||
onSelect: () => void;
|
||||
}) {
|
||||
const uniqueSources = new Set<ValidSources>();
|
||||
documentSet.cc_pair_descriptors.forEach((ccPairDescriptor) => {
|
||||
uniqueSources.add(ccPairDescriptor.connector.source);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
key={documentSet.id}
|
||||
className={
|
||||
`
|
||||
w-72
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-border
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected ? " bg-hover" : " bg-background hover:bg-hover-light")
|
||||
}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div className="flex w-full">
|
||||
<div className="flex flex-col h-full">
|
||||
<div className="font-bold">{documentSet.name}</div>
|
||||
<div className="text-xs">{documentSet.description}</div>
|
||||
<div className="flex gap-x-2 pt-1 mt-auto mb-1">
|
||||
{Array.from(uniqueSources).map((source) => (
|
||||
<SourceIcon key={source} sourceType={source} iconSize={16} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="ml-auto my-auto">
|
||||
<div className="pl-1">
|
||||
<CustomCheckbox checked={isSelected} onChange={() => null} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@ -5,18 +5,17 @@ import { logout } from "@/lib/user";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { CustomDropdown, DefaultDropdownElement } from "./Dropdown";
|
||||
import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { CustomDropdown, DefaultDropdownElement } from "../Dropdown";
|
||||
import { FiMessageSquare, FiSearch } from "react-icons/fi";
|
||||
import { usePathname } from "next/navigation";
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { HeaderWrapper } from "./HeaderWrapper";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
interface HeaderProps {
|
||||
user: User | null;
|
||||
settings: Settings | null;
|
||||
}
|
||||
|
||||
export function Header({ user, settings }: HeaderProps) {
|
||||
export function Header({ user }: HeaderProps) {
|
||||
const router = useRouter();
|
||||
const [dropdownOpen, setDropdownOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
@ -54,9 +53,15 @@ export function Header({ user, settings }: HeaderProps) {
|
||||
};
|
||||
}, [dropdownOpen]);
|
||||
|
||||
const combinedSettings = useContext(SettingsContext);
|
||||
if (!combinedSettings) {
|
||||
return null;
|
||||
}
|
||||
const settings = combinedSettings.settings;
|
||||
|
||||
return (
|
||||
<header className="border-b border-border bg-background-emphasis">
|
||||
<div className="mx-8 flex h-16">
|
||||
<HeaderWrapper>
|
||||
<div className="flex h-full">
|
||||
<Link
|
||||
className="py-4"
|
||||
href={
|
||||
@ -133,7 +138,7 @@ export function Header({ user, settings }: HeaderProps) {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
</HeaderWrapper>
|
||||
);
|
||||
}
|
||||
|
11
web/src/components/header/HeaderWrapper.tsx
Normal file
11
web/src/components/header/HeaderWrapper.tsx
Normal file
@ -0,0 +1,11 @@
|
||||
export function HeaderWrapper({
|
||||
children,
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
}) {
|
||||
return (
|
||||
<header className="border-b border-border bg-background-emphasis">
|
||||
<div className="mx-8 h-16">{children}</div>
|
||||
</header>
|
||||
);
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { CustomDropdown, DefaultDropdownElement } from "../Dropdown";
|
||||
import { FiChevronDown } from "react-icons/fi";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRef, useState } from "react";
|
||||
import { useContext, useRef, useState } from "react";
|
||||
import { SearchBar } from "./SearchBar";
|
||||
import { SearchResultsDisplay } from "./SearchResultsDisplay";
|
||||
import { SourceSelector } from "./filtering/Filters";
|
||||
@ -20,9 +20,11 @@ import { SearchHelper } from "./SearchHelper";
|
||||
import { CancellationToken, cancellable } from "@/lib/search/cancellable";
|
||||
import { useFilters, useObjectState } from "@/lib/hooks";
|
||||
import { questionValidationStreamed } from "@/lib/search/streamingQuestionValidation";
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { PersonaSelector } from "./PersonaSelector";
|
||||
import { computeAvailableFilters } from "@/lib/filters";
|
||||
import { useRouter } from "next/router";
|
||||
import { SettingsContext } from "../settings/SettingsProvider";
|
||||
|
||||
const SEARCH_DEFAULT_OVERRIDES_START: SearchDefaultOverrides = {
|
||||
forceDisplayQA: false,
|
||||
@ -211,6 +213,16 @@ export const SearchSection = ({
|
||||
setIsFetching(false);
|
||||
};
|
||||
|
||||
// handle redirect if search page is disabled
|
||||
// NOTE: this must be done here, in a client component since
|
||||
// settings are passed in via Context and therefore aren't
|
||||
// available in server-side components
|
||||
const router = useRouter();
|
||||
const settings = useContext(SettingsContext);
|
||||
if (settings?.settings?.search_page_enabled === false) {
|
||||
router.push("/chat");
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="relative max-w-[2000px] xl:max-w-[1430px] mx-auto">
|
||||
<div className="absolute left-0 hidden 2xl:block w-52 3xl:w-64">
|
||||
|
20
web/src/components/settings/SettingsProvider.tsx
Normal file
20
web/src/components/settings/SettingsProvider.tsx
Normal file
@ -0,0 +1,20 @@
|
||||
"use client";
|
||||
|
||||
import { CombinedSettings } from "@/app/admin/settings/interfaces";
|
||||
import { createContext } from "react";
|
||||
|
||||
export const SettingsContext = createContext<CombinedSettings | null>(null);
|
||||
|
||||
export function SettingsProvider({
|
||||
children,
|
||||
settings,
|
||||
}: {
|
||||
children: React.ReactNode | JSX.Element;
|
||||
settings: CombinedSettings;
|
||||
}) {
|
||||
return (
|
||||
<SettingsContext.Provider value={settings}>
|
||||
{children}
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
30
web/src/components/settings/lib.ts
Normal file
30
web/src/components/settings/lib.ts
Normal file
@ -0,0 +1,30 @@
|
||||
import { EnterpriseSettings, Settings } from "@/app/admin/settings/interfaces";
|
||||
import { CUSTOM_ANALYTICS_ENABLED, EE_ENABLED } from "@/lib/constants";
|
||||
import { fetchSS } from "@/lib/utilsSS";
|
||||
|
||||
export async function fetchSettingsSS() {
|
||||
const tasks = [fetchSS("/settings")];
|
||||
if (EE_ENABLED) {
|
||||
tasks.push(fetchSS("/enterprise-settings"));
|
||||
if (CUSTOM_ANALYTICS_ENABLED) {
|
||||
tasks.push(fetchSS("/enterprise-settings/custom-analytics-script"));
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(tasks);
|
||||
|
||||
const settings = (await results[0].json()) as Settings;
|
||||
const enterpriseSettings =
|
||||
tasks.length > 1 ? ((await results[1].json()) as EnterpriseSettings) : null;
|
||||
const customAnalyticsScript = (
|
||||
tasks.length > 2 ? await results[2].json() : null
|
||||
) as string | null;
|
||||
|
||||
const combinedSettings = {
|
||||
settings,
|
||||
enterpriseSettings,
|
||||
customAnalyticsScript,
|
||||
};
|
||||
|
||||
return combinedSettings;
|
||||
}
|
103
web/src/lib/assistants/fetchPersonaEditorInfoSS.ts
Normal file
103
web/src/lib/assistants/fetchPersonaEditorInfoSS.ts
Normal file
@ -0,0 +1,103 @@
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { CCPairBasicInfo, DocumentSet, User } from "../types";
|
||||
import { getCurrentUserSS } from "../userSS";
|
||||
import { fetchSS } from "../utilsSS";
|
||||
|
||||
export async function fetchPersonaEditorInfoSS(
|
||||
personaId?: number | string
|
||||
): Promise<
|
||||
| [
|
||||
{
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
documentSets: DocumentSet[];
|
||||
llmOverrideOptions: string[];
|
||||
defaultLLM: string;
|
||||
user: User | null;
|
||||
existingPersona: Persona | null;
|
||||
},
|
||||
null,
|
||||
]
|
||||
| [null, string]
|
||||
> {
|
||||
const tasks = [
|
||||
fetchSS("/manage/indexing-status"),
|
||||
fetchSS("/manage/document-set"),
|
||||
fetchSS("/persona/utils/list-available-models"),
|
||||
fetchSS("/persona/utils/default-model"),
|
||||
// duplicate fetch, but shouldn't be too big of a deal
|
||||
// this page is not a high traffic page
|
||||
getCurrentUserSS(),
|
||||
];
|
||||
if (personaId) {
|
||||
tasks.push(fetchSS(`/persona/${personaId}`));
|
||||
} else {
|
||||
tasks.push((async () => null)());
|
||||
}
|
||||
|
||||
const [
|
||||
ccPairsInfoResponse,
|
||||
documentSetsResponse,
|
||||
llmOverridesResponse,
|
||||
defaultLLMResponse,
|
||||
user,
|
||||
personaResponse,
|
||||
] = (await Promise.all(tasks)) as [
|
||||
Response,
|
||||
Response,
|
||||
Response,
|
||||
Response,
|
||||
User | null,
|
||||
Response | null,
|
||||
];
|
||||
|
||||
if (!ccPairsInfoResponse.ok) {
|
||||
return [
|
||||
null,
|
||||
`Failed to fetch connectors - ${await ccPairsInfoResponse.text()}`,
|
||||
];
|
||||
}
|
||||
const ccPairs = (await ccPairsInfoResponse.json()) as CCPairBasicInfo[];
|
||||
|
||||
if (!documentSetsResponse.ok) {
|
||||
return [
|
||||
null,
|
||||
`Failed to fetch document sets - ${await documentSetsResponse.text()}`,
|
||||
];
|
||||
}
|
||||
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
|
||||
|
||||
if (!llmOverridesResponse.ok) {
|
||||
return [
|
||||
null,
|
||||
`Failed to fetch LLM override options - ${await llmOverridesResponse.text()}`,
|
||||
];
|
||||
}
|
||||
const llmOverrideOptions = (await llmOverridesResponse.json()) as string[];
|
||||
|
||||
if (!defaultLLMResponse.ok) {
|
||||
return [
|
||||
null,
|
||||
`Failed to fetch default LLM - ${await defaultLLMResponse.text()}`,
|
||||
];
|
||||
}
|
||||
const defaultLLM = (await defaultLLMResponse.json()) as string;
|
||||
|
||||
if (personaId && personaResponse && !personaResponse.ok) {
|
||||
return [null, `Failed to fetch Persona - ${await personaResponse.text()}`];
|
||||
}
|
||||
const existingPersona = personaResponse
|
||||
? ((await personaResponse.json()) as Persona)
|
||||
: null;
|
||||
|
||||
return [
|
||||
{
|
||||
ccPairs,
|
||||
documentSets,
|
||||
llmOverrideOptions,
|
||||
defaultLLM,
|
||||
user,
|
||||
existingPersona,
|
||||
},
|
||||
null,
|
||||
];
|
||||
}
|
@ -23,3 +23,8 @@ export const HEADER_PADDING = "pt-[64px]";
|
||||
// can be the single source of truth
|
||||
export const EE_ENABLED =
|
||||
process.env.NEXT_PUBLIC_ENABLE_PAID_EE_FEATURES?.toLowerCase() === "true";
|
||||
|
||||
// Enterprise-only settings
|
||||
export const CUSTOM_ANALYTICS_ENABLED = process.env.CUSTOM_ANALYTICS_SECRET_KEY
|
||||
? true
|
||||
: false;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { DocumentSet, ValidSources } from "./types";
|
||||
import { getSourcesForPersona } from "./sources";
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { DateRangePickerValue } from "@tremor/react";
|
||||
import { Tag, ValidSources } from "../types";
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
|
||||
export const FlowType = {
|
||||
SEARCH: "search",
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { fetchSS } from "./utilsSS";
|
||||
|
||||
export async function getSettingsSS(): Promise<Settings | null> {
|
||||
const response = await fetchSS("/settings");
|
||||
if (response.ok) {
|
||||
return await response.json();
|
||||
}
|
||||
return null;
|
||||
}
|
@ -27,7 +27,7 @@ import {
|
||||
} from "@/components/icons/icons";
|
||||
import { ValidSources } from "./types";
|
||||
import { SourceCategory, SourceMetadata } from "./search/interfaces";
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
|
||||
interface PartialSourceMetadata {
|
||||
icon: React.FC<{ size?: number; className?: string }>;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
@ -9,6 +9,11 @@ export interface User {
|
||||
role: "basic" | "admin";
|
||||
}
|
||||
|
||||
export interface MinimalUserSnapshot {
|
||||
id: string;
|
||||
email: string;
|
||||
}
|
||||
|
||||
export type ValidSources =
|
||||
| "web"
|
||||
| "github"
|
||||
|
Loading…
x
Reference in New Issue
Block a user