From 459bd468466d158bf71735b2700786d391fab3ed Mon Sep 17 00:00:00 2001
From: pablodanswer
Date: Thu, 1 Aug 2024 08:40:35 -0700
Subject: [PATCH] Add Prompt library (#1990)
---
.../e1392f05e840_added_input_prompts.py | 58 ++++
backend/danswer/auth/users.py | 1 +
backend/danswer/chat/input_prompts.yaml | 24 ++
backend/danswer/chat/load_yamls.py | 25 ++
backend/danswer/configs/chat_configs.py | 1 +
backend/danswer/db/input_prompt.py | 202 ++++++++++++++
backend/danswer/db/models.py | 27 ++
backend/danswer/main.py | 6 +
.../server/features/input_prompt/__init__.py | 0
.../server/features/input_prompt/api.py | 134 +++++++++
.../server/features/input_prompt/models.py | 47 ++++
.../danswer/server/features/persona/api.py | 2 +-
web/src/app/admin/prompt-library/hooks.ts | 46 ++++
.../app/admin/prompt-library/interfaces.ts | 31 +++
.../prompt-library/modals/AddPromptModal.tsx | 92 +++++++
.../prompt-library/modals/EditPromptModal.tsx | 138 ++++++++++
web/src/app/admin/prompt-library/page.tsx | 32 +++
.../admin/prompt-library/promptLibrary.tsx | 260 ++++++++++++++++++
.../admin/prompt-library/promptSection.tsx | 146 ++++++++++
web/src/app/admin/settings/page.tsx | 2 +-
web/src/app/assistants/SidebarWrapper.tsx | 6 +-
web/src/app/assistants/gallery/page.tsx | 2 +
.../assistants/mine/WrappedInputPrompts.tsx | 64 +++++
web/src/app/assistants/mine/page.tsx | 2 +
web/src/app/assistants/new/page.tsx | 1 +
web/src/app/chat/ChatPage.tsx | 2 +
web/src/app/chat/input/ChatInputBar.tsx | 179 +++++++++---
web/src/app/chat/modal/ModalWrapper.tsx | 10 +
web/src/app/chat/page.tsx | 2 +
.../chat/sessionSidebar/HistorySidebar.tsx | 12 +
web/src/app/prompts/page.tsx | 40 +++
web/src/components/admin/ClientLayout.tsx | 11 +
web/src/components/admin/Layout.tsx | 16 ++
web/src/components/context/ChatContext.tsx | 2 +
web/src/components/icons/icons.tsx | 24 ++
.../search/filtering/FilterDropdown.tsx | 3 +-
web/src/lib/chat/fetchChatData.ts | 17 +-
37 files changed, 1619 insertions(+), 48 deletions(-)
create mode 100644 backend/alembic/versions/e1392f05e840_added_input_prompts.py
create mode 100644 backend/danswer/chat/input_prompts.yaml
create mode 100644 backend/danswer/db/input_prompt.py
create mode 100644 backend/danswer/server/features/input_prompt/__init__.py
create mode 100644 backend/danswer/server/features/input_prompt/api.py
create mode 100644 backend/danswer/server/features/input_prompt/models.py
create mode 100644 web/src/app/admin/prompt-library/hooks.ts
create mode 100644 web/src/app/admin/prompt-library/interfaces.ts
create mode 100644 web/src/app/admin/prompt-library/modals/AddPromptModal.tsx
create mode 100644 web/src/app/admin/prompt-library/modals/EditPromptModal.tsx
create mode 100644 web/src/app/admin/prompt-library/page.tsx
create mode 100644 web/src/app/admin/prompt-library/promptLibrary.tsx
create mode 100644 web/src/app/admin/prompt-library/promptSection.tsx
create mode 100644 web/src/app/assistants/mine/WrappedInputPrompts.tsx
create mode 100644 web/src/app/prompts/page.tsx
diff --git a/backend/alembic/versions/e1392f05e840_added_input_prompts.py b/backend/alembic/versions/e1392f05e840_added_input_prompts.py
new file mode 100644
index 000000000..dd358220f
--- /dev/null
+++ b/backend/alembic/versions/e1392f05e840_added_input_prompts.py
@@ -0,0 +1,58 @@
+"""Added input prompts
+
+Revision ID: e1392f05e840
+Revises: 08a1eda20fe1
+Create Date: 2024-07-13 19:09:22.556224
+
+"""
+
+import fastapi_users_db_sqlalchemy
+
+from alembic import op
+import sqlalchemy as sa
+
+# revision identifiers, used by Alembic.
+revision = "e1392f05e840"
+down_revision = "08a1eda20fe1"
+branch_labels: None = None
+depends_on: None = None
+
+
+def upgrade() -> None:
+ op.create_table(
+ "inputprompt",
+ sa.Column("id", sa.Integer(), autoincrement=True, nullable=False),
+ sa.Column("prompt", sa.String(), nullable=False),
+ sa.Column("content", sa.String(), nullable=False),
+ sa.Column("active", sa.Boolean(), nullable=False),
+ sa.Column("is_public", sa.Boolean(), nullable=False),
+ sa.Column(
+ "user_id",
+ fastapi_users_db_sqlalchemy.generics.GUID(),
+ nullable=True,
+ ),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["user.id"],
+ ),
+ sa.PrimaryKeyConstraint("id"),
+ )
+ op.create_table(
+ "inputprompt__user",
+ sa.Column("input_prompt_id", sa.Integer(), nullable=False),
+ sa.Column("user_id", sa.Integer(), nullable=False),
+ sa.ForeignKeyConstraint(
+ ["input_prompt_id"],
+ ["inputprompt.id"],
+ ),
+ sa.ForeignKeyConstraint(
+ ["user_id"],
+ ["inputprompt.id"],
+ ),
+ sa.PrimaryKeyConstraint("input_prompt_id", "user_id"),
+ )
+
+
+def downgrade() -> None:
+ op.drop_table("inputprompt__user")
+ op.drop_table("inputprompt")
diff --git a/backend/danswer/auth/users.py b/backend/danswer/auth/users.py
index b6f00424d..8c9570c46 100644
--- a/backend/danswer/auth/users.py
+++ b/backend/danswer/auth/users.py
@@ -371,4 +371,5 @@ async def current_admin_user(user: User | None = Depends(current_user)) -> User
status_code=status.HTTP_403_FORBIDDEN,
detail="Access denied. User is not an admin.",
)
+
return user
diff --git a/backend/danswer/chat/input_prompts.yaml b/backend/danswer/chat/input_prompts.yaml
new file mode 100644
index 000000000..cc7dbe78e
--- /dev/null
+++ b/backend/danswer/chat/input_prompts.yaml
@@ -0,0 +1,24 @@
+input_prompts:
+ - id: -5
+ prompt: "Elaborate"
+ content: "Elaborate on the above, give me a more in depth explanation."
+ active: true
+ is_public: true
+
+ - id: -4
+ prompt: "Reword"
+ content: "Help me rewrite the following politely and concisely for professional communication:\n"
+ active: true
+ is_public: true
+
+ - id: -3
+ prompt: "Email"
+ content: "Write a professional email for me including a subject line, signature, etc. Template the parts that need editing with [ ]. The email should cover the following points:\n"
+ active: true
+ is_public: true
+
+ - id: -2
+ prompt: "Debug"
+ content: "Provide step-by-step troubleshooting instructions for the following issue:\n"
+ active: true
+ is_public: true
diff --git a/backend/danswer/chat/load_yamls.py b/backend/danswer/chat/load_yamls.py
index 279ef2b62..ed0c0a2d7 100644
--- a/backend/danswer/chat/load_yamls.py
+++ b/backend/danswer/chat/load_yamls.py
@@ -1,11 +1,13 @@
import yaml
from sqlalchemy.orm import Session
+from danswer.configs.chat_configs import INPUT_PROMPT_YAML
from danswer.configs.chat_configs import MAX_CHUNKS_FED_TO_CHAT
from danswer.configs.chat_configs import PERSONAS_YAML
from danswer.configs.chat_configs import PROMPTS_YAML
from danswer.db.document_set import get_or_create_document_set_by_name
from danswer.db.engine import get_sqlalchemy_engine
+from danswer.db.input_prompt import insert_input_prompt_if_not_exists
from danswer.db.models import DocumentSet as DocumentSetDBModel
from danswer.db.models import Prompt as PromptDBModel
from danswer.db.persona import get_prompt_by_name
@@ -101,9 +103,32 @@ def load_personas_from_yaml(
)
+def load_input_prompts_from_yaml(input_prompts_yaml: str = INPUT_PROMPT_YAML) -> None:
+ with open(input_prompts_yaml, "r") as file:
+ data = yaml.safe_load(file)
+
+ all_input_prompts = data.get("input_prompts", [])
+ with Session(get_sqlalchemy_engine()) as db_session:
+ for input_prompt in all_input_prompts:
+ # If these prompts are deleted (which is a hard delete in the DB), on server startup
+ # they will be recreated, but the user can always just deactivate them, just a light inconvenience
+ insert_input_prompt_if_not_exists(
+ user=None,
+ input_prompt_id=input_prompt.get("id"),
+ prompt=input_prompt["prompt"],
+ content=input_prompt["content"],
+ is_public=input_prompt["is_public"],
+ active=input_prompt.get("active", True),
+ db_session=db_session,
+ commit=True,
+ )
+
+
def load_chat_yamls(
prompt_yaml: str = PROMPTS_YAML,
personas_yaml: str = PERSONAS_YAML,
+ input_prompts_yaml: str = INPUT_PROMPT_YAML,
) -> None:
load_prompts_from_yaml(prompt_yaml)
load_personas_from_yaml(personas_yaml)
+ load_input_prompts_from_yaml(input_prompts_yaml)
diff --git a/backend/danswer/configs/chat_configs.py b/backend/danswer/configs/chat_configs.py
index 0d8aadb6f..3ba8e7a42 100644
--- a/backend/danswer/configs/chat_configs.py
+++ b/backend/danswer/configs/chat_configs.py
@@ -3,6 +3,7 @@ import os
PROMPTS_YAML = "./danswer/chat/prompts.yaml"
PERSONAS_YAML = "./danswer/chat/personas.yaml"
+INPUT_PROMPT_YAML = "./danswer/chat/input_prompts.yaml"
NUM_RETURNED_HITS = 50
# Used for LLM filtering and reranking
diff --git a/backend/danswer/db/input_prompt.py b/backend/danswer/db/input_prompt.py
new file mode 100644
index 000000000..efa54d986
--- /dev/null
+++ b/backend/danswer/db/input_prompt.py
@@ -0,0 +1,202 @@
+from uuid import UUID
+
+from fastapi import HTTPException
+from sqlalchemy import select
+from sqlalchemy.orm import Session
+
+from danswer.db.models import InputPrompt
+from danswer.db.models import User
+from danswer.server.features.input_prompt.models import InputPromptSnapshot
+from danswer.server.manage.models import UserInfo
+from danswer.utils.logger import setup_logger
+
+
+logger = setup_logger()
+
+
+def insert_input_prompt_if_not_exists(
+ user: User | None,
+ input_prompt_id: int | None,
+ prompt: str,
+ content: str,
+ active: bool,
+ is_public: bool,
+ db_session: Session,
+ commit: bool = True,
+) -> InputPrompt:
+ if input_prompt_id is not None:
+ input_prompt = (
+ db_session.query(InputPrompt).filter_by(id=input_prompt_id).first()
+ )
+ else:
+ query = db_session.query(InputPrompt).filter(InputPrompt.prompt == prompt)
+ if user:
+ query = query.filter(InputPrompt.user_id == user.id)
+ else:
+ query = query.filter(InputPrompt.user_id.is_(None))
+ input_prompt = query.first()
+
+ if input_prompt is None:
+ input_prompt = InputPrompt(
+ id=input_prompt_id,
+ prompt=prompt,
+ content=content,
+ active=active,
+ is_public=is_public or user is None,
+ user_id=user.id if user else None,
+ )
+ db_session.add(input_prompt)
+
+ if commit:
+ db_session.commit()
+
+ return input_prompt
+
+
+def insert_input_prompt(
+ prompt: str,
+ content: str,
+ is_public: bool,
+ user: User | None,
+ db_session: Session,
+) -> InputPrompt:
+ input_prompt = InputPrompt(
+ prompt=prompt,
+ content=content,
+ active=True,
+ is_public=is_public or user is None,
+ user_id=user.id if user is not None else None,
+ )
+ db_session.add(input_prompt)
+ db_session.commit()
+
+ return input_prompt
+
+
+def update_input_prompt(
+ user: User | None,
+ input_prompt_id: int,
+ prompt: str,
+ content: str,
+ active: bool,
+ db_session: Session,
+) -> InputPrompt:
+ input_prompt = db_session.scalar(
+ select(InputPrompt).where(InputPrompt.id == input_prompt_id)
+ )
+ if input_prompt is None:
+ raise ValueError(f"No input prompt with id {input_prompt_id}")
+
+ if not validate_user_prompt_authorization(user, input_prompt):
+ raise HTTPException(status_code=401, detail="You don't own this prompt")
+
+ input_prompt.prompt = prompt
+ input_prompt.content = content
+ input_prompt.active = active
+
+ db_session.commit()
+ return input_prompt
+
+
+def validate_user_prompt_authorization(
+ user: User | None, input_prompt: InputPrompt
+) -> bool:
+ prompt = InputPromptSnapshot.from_model(input_prompt=input_prompt)
+
+ if prompt.user_id is not None:
+ if user is None:
+ return False
+
+ user_details = UserInfo.from_model(user)
+ if str(user_details.id) != str(prompt.user_id):
+ return False
+ return True
+
+
+def remove_public_input_prompt(input_prompt_id: int, db_session: Session) -> None:
+ input_prompt = db_session.scalar(
+ select(InputPrompt).where(InputPrompt.id == input_prompt_id)
+ )
+
+ if input_prompt is None:
+ raise ValueError(f"No input prompt with id {input_prompt_id}")
+
+ if not input_prompt.is_public:
+ raise HTTPException(status_code=400, detail="This prompt is not public")
+
+ db_session.delete(input_prompt)
+ db_session.commit()
+
+
+def remove_input_prompt(
+ user: User | None, input_prompt_id: int, db_session: Session
+) -> None:
+ input_prompt = db_session.scalar(
+ select(InputPrompt).where(InputPrompt.id == input_prompt_id)
+ )
+ if input_prompt is None:
+ raise ValueError(f"No input prompt with id {input_prompt_id}")
+
+ if input_prompt.is_public:
+ raise HTTPException(
+ status_code=400, detail="Cannot delete public prompts with this method"
+ )
+
+ if not validate_user_prompt_authorization(user, input_prompt):
+ raise HTTPException(status_code=401, detail="You do not own this prompt")
+
+ db_session.delete(input_prompt)
+ db_session.commit()
+
+
+def fetch_input_prompt_by_id(
+ id: int, user_id: UUID | None, db_session: Session
+) -> InputPrompt:
+ query = select(InputPrompt).where(InputPrompt.id == id)
+
+ if user_id:
+ query = query.where(
+ (InputPrompt.user_id == user_id) | (InputPrompt.user_id is None)
+ )
+ else:
+ # If no user_id is provided, only fetch prompts without a user_id (aka public)
+ query = query.where(InputPrompt.user_id == None) # noqa
+
+ result = db_session.scalar(query)
+
+ if result is None:
+ raise HTTPException(422, "No input prompt found")
+
+ return result
+
+
+def fetch_public_input_prompts(
+ db_session: Session,
+) -> list[InputPrompt]:
+ query = select(InputPrompt).where(InputPrompt.is_public)
+ return list(db_session.scalars(query).all())
+
+
+def fetch_input_prompts_by_user(
+ db_session: Session,
+ user_id: UUID | None,
+ active: bool | None = None,
+ include_public: bool = False,
+) -> list[InputPrompt]:
+ query = select(InputPrompt)
+
+ if user_id is not None:
+ if include_public:
+ query = query.where(
+ (InputPrompt.user_id == user_id) | InputPrompt.is_public
+ )
+ else:
+ query = query.where(InputPrompt.user_id == user_id)
+
+ elif include_public:
+ query = query.where(InputPrompt.is_public)
+
+ if active is not None:
+ query = query.where(InputPrompt.active == active)
+
+ return list(db_session.scalars(query).all())
diff --git a/backend/danswer/db/models.py b/backend/danswer/db/models.py
index 9f372811c..397d26ac7 100644
--- a/backend/danswer/db/models.py
+++ b/backend/danswer/db/models.py
@@ -137,12 +137,39 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
)
prompts: Mapped[list["Prompt"]] = relationship("Prompt", back_populates="user")
+ input_prompts: Mapped[list["InputPrompt"]] = relationship(
+ "InputPrompt", back_populates="user"
+ )
+
# Personas owned by this user
personas: Mapped[list["Persona"]] = relationship("Persona", back_populates="user")
# Custom tools created by this user
custom_tools: Mapped[list["Tool"]] = relationship("Tool", back_populates="user")
+class InputPrompt(Base):
+ __tablename__ = "inputprompt"
+
+ id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
+ prompt: Mapped[str] = mapped_column(String)
+ content: Mapped[str] = mapped_column(String)
+ active: Mapped[bool] = mapped_column(Boolean)
+ user: Mapped[User | None] = relationship("User", back_populates="input_prompts")
+ is_public: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
+ user_id: Mapped[UUID] = mapped_column(ForeignKey("user.id"))
+
+
+class InputPrompt__User(Base):
+ __tablename__ = "inputprompt__user"
+
+ input_prompt_id: Mapped[int] = mapped_column(
+ ForeignKey("inputprompt.id"), primary_key=True
+ )
+ user_id: Mapped[UUID] = mapped_column(
+ ForeignKey("inputprompt.id"), primary_key=True
+ )
+
+
class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base):
pass
diff --git a/backend/danswer/main.py b/backend/danswer/main.py
index 1d4c7635a..5cfe1c1c1 100644
--- a/backend/danswer/main.py
+++ b/backend/danswer/main.py
@@ -62,6 +62,10 @@ from danswer.server.documents.credential import router as credential_router
from danswer.server.documents.document import router as document_router
from danswer.server.features.document_set.api import router as document_set_router
from danswer.server.features.folder.api import router as folder_router
+from danswer.server.features.input_prompt.api import (
+ admin_router as admin_input_prompt_router,
+)
+from danswer.server.features.input_prompt.api import basic_router as input_prompt_router
from danswer.server.features.persona.api import admin_router as admin_persona_router
from danswer.server.features.persona.api import basic_router as persona_router
from danswer.server.features.prompt.api import basic_router as prompt_router
@@ -286,6 +290,8 @@ def get_application() -> FastAPI:
include_router_with_global_prefix_prepended(application, standard_answer_router)
include_router_with_global_prefix_prepended(application, persona_router)
include_router_with_global_prefix_prepended(application, admin_persona_router)
+ include_router_with_global_prefix_prepended(application, input_prompt_router)
+ include_router_with_global_prefix_prepended(application, admin_input_prompt_router)
include_router_with_global_prefix_prepended(application, prompt_router)
include_router_with_global_prefix_prepended(application, tool_router)
include_router_with_global_prefix_prepended(application, admin_tool_router)
diff --git a/backend/danswer/server/features/input_prompt/__init__.py b/backend/danswer/server/features/input_prompt/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/backend/danswer/server/features/input_prompt/api.py b/backend/danswer/server/features/input_prompt/api.py
new file mode 100644
index 000000000..58eecd009
--- /dev/null
+++ b/backend/danswer/server/features/input_prompt/api.py
@@ -0,0 +1,134 @@
+from fastapi import APIRouter
+from fastapi import Depends
+from fastapi import HTTPException
+from sqlalchemy.orm import Session
+
+from danswer.auth.users import current_admin_user
+from danswer.auth.users import current_user
+from danswer.db.engine import get_session
+from danswer.db.input_prompt import fetch_input_prompt_by_id
+from danswer.db.input_prompt import fetch_input_prompts_by_user
+from danswer.db.input_prompt import fetch_public_input_prompts
+from danswer.db.input_prompt import insert_input_prompt
+from danswer.db.input_prompt import remove_input_prompt
+from danswer.db.input_prompt import remove_public_input_prompt
+from danswer.db.input_prompt import update_input_prompt
+from danswer.db.models import User
+from danswer.server.features.input_prompt.models import CreateInputPromptRequest
+from danswer.server.features.input_prompt.models import InputPromptSnapshot
+from danswer.server.features.input_prompt.models import UpdateInputPromptRequest
+from danswer.utils.logger import setup_logger
+
+logger = setup_logger()
+
+basic_router = APIRouter(prefix="/input_prompt")
+admin_router = APIRouter(prefix="/admin/input_prompt")
+
+
+@basic_router.get("")
+def list_input_prompts(
+ user: User | None = Depends(current_user),
+ include_public: bool = False,
+ db_session: Session = Depends(get_session),
+) -> list[InputPromptSnapshot]:
+ user_prompts = fetch_input_prompts_by_user(
+ user_id=user.id if user is not None else None,
+ db_session=db_session,
+ include_public=include_public,
+ )
+ return [InputPromptSnapshot.from_model(prompt) for prompt in user_prompts]
+
+
+@basic_router.get("/{input_prompt_id}")
+def get_input_prompt(
+ input_prompt_id: int,
+ user: User | None = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> InputPromptSnapshot:
+ input_prompt = fetch_input_prompt_by_id(
+ id=input_prompt_id,
+ user_id=user.id if user is not None else None,
+ db_session=db_session,
+ )
+ return InputPromptSnapshot.from_model(input_prompt=input_prompt)
+
+
+@basic_router.post("")
+def create_input_prompt(
+ create_input_prompt_request: CreateInputPromptRequest,
+ user: User | None = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> InputPromptSnapshot:
+ input_prompt = insert_input_prompt(
+ prompt=create_input_prompt_request.prompt,
+ content=create_input_prompt_request.content,
+ is_public=create_input_prompt_request.is_public,
+ user=user,
+ db_session=db_session,
+ )
+ return InputPromptSnapshot.from_model(input_prompt)
+
+
+@basic_router.patch("/{input_prompt_id}")
+def patch_input_prompt(
+ input_prompt_id: int,
+ update_input_prompt_request: UpdateInputPromptRequest,
+ user: User | None = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> InputPromptSnapshot:
+ try:
+ updated_input_prompt = update_input_prompt(
+ user=user,
+ input_prompt_id=input_prompt_id,
+ prompt=update_input_prompt_request.prompt,
+ content=update_input_prompt_request.content,
+ active=update_input_prompt_request.active,
+ db_session=db_session,
+ )
+ except ValueError as e:
+ error_msg = "Error occurred while updated input prompt"
+ logger.warn(f"{error_msg}. Stack trace: {e}")
+ raise HTTPException(status_code=404, detail=error_msg)
+
+ return InputPromptSnapshot.from_model(updated_input_prompt)
+
+
+@basic_router.delete("/{input_prompt_id}")
+def delete_input_prompt(
+ input_prompt_id: int,
+ user: User | None = Depends(current_user),
+ db_session: Session = Depends(get_session),
+) -> None:
+ try:
+ remove_input_prompt(user, input_prompt_id, db_session)
+
+ except ValueError as e:
+ error_msg = "Error occurred while deleting input prompt"
+ logger.warn(f"{error_msg}. Stack trace: {e}")
+ raise HTTPException(status_code=404, detail=error_msg)
+
+
+@admin_router.delete("/{input_prompt_id}")
+def delete_public_input_prompt(
+ input_prompt_id: int,
+ _: User | None = Depends(current_admin_user),
+ db_session: Session = Depends(get_session),
+) -> None:
+ try:
+ remove_public_input_prompt(input_prompt_id, db_session)
+
+ except ValueError as e:
+ error_msg = "Error occurred while deleting input prompt"
+ logger.warn(f"{error_msg}. Stack trace: {e}")
+ raise HTTPException(status_code=404, detail=error_msg)
+
+
+@admin_router.get("")
+def list_public_input_prompts(
+ _: User | None = Depends(current_admin_user),
+ db_session: Session = Depends(get_session),
+) -> list[InputPromptSnapshot]:
+ user_prompts = fetch_public_input_prompts(
+ db_session=db_session,
+ )
+ return [InputPromptSnapshot.from_model(prompt) for prompt in user_prompts]
diff --git a/backend/danswer/server/features/input_prompt/models.py b/backend/danswer/server/features/input_prompt/models.py
new file mode 100644
index 000000000..21ce2ba4e
--- /dev/null
+++ b/backend/danswer/server/features/input_prompt/models.py
@@ -0,0 +1,47 @@
+from uuid import UUID
+
+from pydantic import BaseModel
+
+from danswer.db.models import InputPrompt
+from danswer.utils.logger import setup_logger
+
+logger = setup_logger()
+
+
+class CreateInputPromptRequest(BaseModel):
+ prompt: str
+ content: str
+ is_public: bool
+
+
+class UpdateInputPromptRequest(BaseModel):
+ prompt: str
+ content: str
+ active: bool
+
+
+class InputPromptResponse(BaseModel):
+ id: int
+ prompt: str
+ content: str
+ active: bool
+
+
+class InputPromptSnapshot(BaseModel):
+ id: int
+ prompt: str
+ content: str
+ active: bool
+ user_id: UUID | None
+ is_public: bool
+
+ @classmethod
+ def from_model(cls, input_prompt: InputPrompt) -> "InputPromptSnapshot":
+ return InputPromptSnapshot(
+ id=input_prompt.id,
+ prompt=input_prompt.prompt,
+ content=input_prompt.content,
+ active=input_prompt.active,
+ user_id=input_prompt.user_id,
+ is_public=input_prompt.is_public,
+ )
diff --git a/backend/danswer/server/features/persona/api.py b/backend/danswer/server/features/persona/api.py
index 87e9fbdab..cf2c0e261 100644
--- a/backend/danswer/server/features/persona/api.py
+++ b/backend/danswer/server/features/persona/api.py
@@ -96,7 +96,7 @@ def undelete_persona(
)
-# used for assistnat profile pictures
+# used for assistat profile pictures
@admin_router.post("/upload-image")
def upload_file(
file: UploadFile,
diff --git a/web/src/app/admin/prompt-library/hooks.ts b/web/src/app/admin/prompt-library/hooks.ts
new file mode 100644
index 000000000..ccab6b340
--- /dev/null
+++ b/web/src/app/admin/prompt-library/hooks.ts
@@ -0,0 +1,46 @@
+import useSWR from "swr";
+import { InputPrompt } from "./interfaces";
+
+const fetcher = (url: string) => fetch(url).then((res) => res.json());
+
+export const useAdminInputPrompts = () => {
+ const { data, error, mutate } = useSWR(
+ `/api/admin/input_prompt`,
+ fetcher
+ );
+
+ return {
+ data,
+ error,
+ isLoading: !error && !data,
+ refreshInputPrompts: mutate,
+ };
+};
+
+export const useInputPrompts = (includePublic: boolean = false) => {
+ const { data, error, mutate } = useSWR(
+ `/api/input_prompt${includePublic ? "?include_public=true" : ""}`,
+ fetcher
+ );
+
+ return {
+ data,
+ error,
+ isLoading: !error && !data,
+ refreshInputPrompts: mutate,
+ };
+};
+
+export const useInputPrompt = (id: number) => {
+ const { data, error, mutate } = useSWR(
+ `/api/input_prompt/${id}`,
+ fetcher
+ );
+
+ return {
+ data,
+ error,
+ isLoading: !error && !data,
+ refreshInputPrompt: mutate,
+ };
+};
diff --git a/web/src/app/admin/prompt-library/interfaces.ts b/web/src/app/admin/prompt-library/interfaces.ts
new file mode 100644
index 000000000..9143a0ea8
--- /dev/null
+++ b/web/src/app/admin/prompt-library/interfaces.ts
@@ -0,0 +1,31 @@
+export interface InputPrompt {
+ id: number;
+ prompt: string;
+ content: string;
+ active: boolean;
+ is_public: string;
+}
+
+export interface EditPromptModalProps {
+ onClose: () => void;
+
+ promptId: number;
+ editInputPrompt: (
+ promptId: number,
+ values: CreateInputPromptRequest
+ ) => Promise;
+}
+export interface CreateInputPromptRequest {
+ prompt: string;
+ content: string;
+}
+
+export interface AddPromptModalProps {
+ onClose: () => void;
+ onSubmit: (promptData: CreateInputPromptRequest) => void;
+}
+export interface PromptData {
+ id: number;
+ prompt: string;
+ content: string;
+}
diff --git a/web/src/app/admin/prompt-library/modals/AddPromptModal.tsx b/web/src/app/admin/prompt-library/modals/AddPromptModal.tsx
new file mode 100644
index 000000000..bc75c17c0
--- /dev/null
+++ b/web/src/app/admin/prompt-library/modals/AddPromptModal.tsx
@@ -0,0 +1,92 @@
+import React from "react";
+import { Formik, Form, Field, ErrorMessage } from "formik";
+import * as Yup from "yup";
+import { ModalWrapper } from "@/app/chat/modal/ModalWrapper";
+import { Button, Textarea, TextInput } from "@tremor/react";
+
+import { BookstackIcon } from "@/components/icons/icons";
+import { AddPromptModalProps } from "../interfaces";
+import { TextFormField } from "@/components/admin/connectors/Field";
+
+const AddPromptSchema = Yup.object().shape({
+ title: Yup.string().required("Title is required"),
+ prompt: Yup.string().required("Prompt is required"),
+});
+
+const AddPromptModal = ({ onClose, onSubmit }: AddPromptModalProps) => {
+ const defaultPrompts = [
+ {
+ title: "Email help",
+ prompt: "Write a professional email addressing the following points:",
+ },
+ {
+ title: "Code explanation",
+ prompt: "Explain the following code snippet in simple terms:",
+ },
+ {
+ title: "Product description",
+ prompt: "Write a compelling product description for the following item:",
+ },
+ {
+ title: "Troubleshooting steps",
+ prompt:
+ "Provide step-by-step troubleshooting instructions for the following issue:",
+ },
+ ];
+
+ return (
+
+ {
+ onSubmit({
+ prompt: values.title,
+ content: values.prompt,
+ });
+ setSubmitting(false);
+ onClose();
+ }}
+ >
+ {({ isSubmitting, setFieldValue }) => (
+
+ )}
+
+
+ );
+};
+
+export default AddPromptModal;
diff --git a/web/src/app/admin/prompt-library/modals/EditPromptModal.tsx b/web/src/app/admin/prompt-library/modals/EditPromptModal.tsx
new file mode 100644
index 000000000..f71dfc487
--- /dev/null
+++ b/web/src/app/admin/prompt-library/modals/EditPromptModal.tsx
@@ -0,0 +1,138 @@
+import React from "react";
+import { Formik, Form, Field, ErrorMessage } from "formik";
+import * as Yup from "yup";
+import { ModalWrapper } from "@/app/chat/modal/ModalWrapper";
+import { Button, Textarea, TextInput } from "@tremor/react";
+import { useInputPrompt } from "../hooks";
+import { EditPromptModalProps } from "../interfaces";
+
+const EditPromptSchema = Yup.object().shape({
+ prompt: Yup.string().required("Title is required"),
+ content: Yup.string().required("Content is required"),
+ active: Yup.boolean(),
+});
+
+const EditPromptModal = ({
+ onClose,
+ promptId,
+ editInputPrompt,
+}: EditPromptModalProps) => {
+ const {
+ data: promptData,
+ error,
+ refreshInputPrompt,
+ } = useInputPrompt(promptId);
+
+ if (error)
+ return (
+
+ Failed to load prompt data
+
+ );
+
+ if (!promptData)
+ return (
+
+ Loading...
+
+ );
+
+ return (
+
+ {
+ editInputPrompt(promptId, values);
+ refreshInputPrompt();
+ }}
+ >
+ {({ isSubmitting, values }) => (
+
+ )}
+
+
+ );
+};
+
+export default EditPromptModal;
diff --git a/web/src/app/admin/prompt-library/page.tsx b/web/src/app/admin/prompt-library/page.tsx
new file mode 100644
index 000000000..d7c72ff5f
--- /dev/null
+++ b/web/src/app/admin/prompt-library/page.tsx
@@ -0,0 +1,32 @@
+"use client";
+
+import { AdminPageTitle } from "@/components/admin/Title";
+import { ClosedBookIcon } from "@/components/icons/icons";
+import { useAdminInputPrompts } from "./hooks";
+import { PromptSection } from "./promptSection";
+
+const Page = () => {
+ const {
+ data: promptLibrary,
+ error: promptLibraryError,
+ isLoading: promptLibraryIsLoading,
+ refreshInputPrompts: refreshPrompts,
+ } = useAdminInputPrompts();
+
+ return (
+
+
}
+ title="Prompt Library"
+ />
+
+
+ );
+};
+export default Page;
diff --git a/web/src/app/admin/prompt-library/promptLibrary.tsx b/web/src/app/admin/prompt-library/promptLibrary.tsx
new file mode 100644
index 000000000..0b47bf40a
--- /dev/null
+++ b/web/src/app/admin/prompt-library/promptLibrary.tsx
@@ -0,0 +1,260 @@
+"use client";
+
+import { EditIcon, TrashIcon } from "@/components/icons/icons";
+import { PopupSpec } from "@/components/admin/connectors/Popup";
+import { MagnifyingGlass } from "@phosphor-icons/react";
+import { useState } from "react";
+import {
+ Table,
+ TableHead,
+ TableRow,
+ TableHeaderCell,
+ TableBody,
+ TableCell,
+} from "@tremor/react";
+import { FilterDropdown } from "@/components/search/filtering/FilterDropdown";
+import { FiTag } from "react-icons/fi";
+import { PageSelector } from "@/components/PageSelector";
+import { InputPrompt } from "./interfaces";
+import { Modal } from "@/components/Modal";
+
+const CategoryBubble = ({
+ name,
+ onDelete,
+}: {
+ name: string;
+ onDelete?: () => void;
+}) => (
+
+ {name}
+ {onDelete && (
+
+ ×
+
+ )}
+
+);
+
+const NUM_RESULTS_PER_PAGE = 10;
+
+export const PromptLibraryTable = ({
+ promptLibrary,
+ refresh,
+ setPopup,
+ handleEdit,
+ isPublic,
+}: {
+ promptLibrary: InputPrompt[];
+ refresh: () => void;
+ setPopup: (popup: PopupSpec | null) => void;
+ handleEdit: (promptId: number) => void;
+ isPublic: boolean;
+}) => {
+ const [query, setQuery] = useState("");
+ const [currentPage, setCurrentPage] = useState(1);
+ const [selectedStatus, setSelectedStatus] = useState([]);
+
+ const columns = [
+ { name: "Prompt", key: "prompt" },
+ { name: "Content", key: "content" },
+ { name: "Status", key: "status" },
+ { name: "", key: "edit" },
+ { name: "", key: "delete" },
+ ];
+
+ const filteredPromptLibrary = promptLibrary.filter((item) => {
+ const cleanedQuery = query.toLowerCase();
+ const searchMatch =
+ item.prompt.toLowerCase().includes(cleanedQuery) ||
+ item.content.toLowerCase().includes(cleanedQuery);
+ const statusMatch =
+ selectedStatus.length === 0 ||
+ (selectedStatus.includes("Active") && item.active) ||
+ (selectedStatus.includes("Inactive") && !item.active);
+
+ return searchMatch && statusMatch;
+ });
+
+ const totalPages = Math.ceil(
+ filteredPromptLibrary.length / NUM_RESULTS_PER_PAGE
+ );
+ const startIndex = (currentPage - 1) * NUM_RESULTS_PER_PAGE;
+ const endIndex = startIndex + NUM_RESULTS_PER_PAGE;
+ const paginatedPromptLibrary = filteredPromptLibrary.slice(
+ startIndex,
+ endIndex
+ );
+
+ const handlePageChange = (page: number) => {
+ setCurrentPage(page);
+ };
+
+ const handleDelete = async (id: number) => {
+ const response = await fetch(
+ `/api${isPublic ? "/admin" : ""}/input_prompt/${id}`,
+ {
+ method: "DELETE",
+ }
+ );
+ if (!response.ok) {
+ setPopup({ message: "Failed to delete input prompt", type: "error" });
+ }
+ refresh();
+ };
+
+ const handleStatusSelect = (status: string) => {
+ setSelectedStatus((prev) => {
+ if (prev.includes(status)) {
+ return prev.filter((s) => s !== status);
+ }
+ return [...prev, status];
+ });
+ };
+
+ const [confirmDeletionId, setConfirmDeletionId] = useState(
+ null
+ );
+
+ return (
+
+ {confirmDeletionId != null && (
+
setConfirmDeletionId(null)}
+ className="max-w-sm"
+ >
+ <>
+
+ Are you sure you want to delete this prompt? You will not be able
+ to recover this prompt
+
+
+ {
+ await handleDelete(confirmDeletionId);
+ setConfirmDeletionId(null);
+ }}
+ >
+ Yes
+
+ setConfirmDeletionId(null)}
+ className="rounded py-1.5 px-2 bg-background-150 text-text-800"
+ >
+ {" "}
+ No
+
+
+ >
+
+ )}
+
+
+
+ {
+ setQuery(event.target.value);
+ setCurrentPage(1);
+ }}
+ />
+
+
+
handleStatusSelect(option.key)}
+ icon={ }
+ defaultDisplay="All Statuses"
+ />
+
+ {selectedStatus.map((status) => (
+ handleStatusSelect(status)}
+ />
+ ))}
+
+
+
+
+
+
+ {columns.map((column) => (
+
+ {column.name}
+
+ ))}
+
+
+
+ {paginatedPromptLibrary.length > 0 ? (
+ paginatedPromptLibrary
+ .filter((prompt) => !(!isPublic && prompt.is_public))
+ .map((item) => (
+
+ {item.prompt}
+ {item.content}
+ {item.active ? "Active" : "Inactive"}
+
+ setConfirmDeletionId(item.id)}
+ >
+
+
+
+
+ handleEdit(item.id)}>
+
+
+
+
+ ))
+ ) : (
+
+ No matching prompts found...
+
+ )}
+
+
+ {paginatedPromptLibrary.length > 0 && (
+
+ )}
+
+
+ );
+};
diff --git a/web/src/app/admin/prompt-library/promptSection.tsx b/web/src/app/admin/prompt-library/promptSection.tsx
new file mode 100644
index 000000000..f719ad500
--- /dev/null
+++ b/web/src/app/admin/prompt-library/promptSection.tsx
@@ -0,0 +1,146 @@
+"use client";
+
+import { usePopup } from "@/components/admin/connectors/Popup";
+import { ThreeDotsLoader } from "@/components/Loading";
+import { ErrorCallout } from "@/components/ErrorCallout";
+import { Button, Divider, Text } from "@tremor/react";
+import { useState } from "react";
+import AddPromptModal from "./modals/AddPromptModal";
+import EditPromptModal from "./modals/EditPromptModal";
+import { PromptLibraryTable } from "./promptLibrary";
+import { CreateInputPromptRequest, InputPrompt } from "./interfaces";
+
+export const PromptSection = ({
+ promptLibrary,
+ isLoading,
+ error,
+ refreshPrompts,
+ centering = false,
+ isPublic,
+}: {
+ promptLibrary: InputPrompt[];
+ isLoading: boolean;
+ error: any;
+ refreshPrompts: () => void;
+ centering?: boolean;
+ isPublic: boolean;
+}) => {
+ const { popup, setPopup } = usePopup();
+ const [newPrompt, setNewPrompt] = useState(false);
+ const [newPromptId, setNewPromptId] = useState(null);
+
+ const createInputPrompt = async (
+ promptData: CreateInputPromptRequest
+ ): Promise => {
+ const response = await fetch("/api/input_prompt", {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({ ...promptData, is_public: isPublic }),
+ });
+
+ if (!response.ok) {
+ setPopup({ message: "Failed to create input prompt", type: "error" });
+ }
+
+ refreshPrompts();
+ return response.json();
+ };
+
+ const editInputPrompt = async (
+ promptId: number,
+ values: CreateInputPromptRequest
+ ) => {
+ try {
+ const response = await fetch(`/api/input_prompt/${promptId}`, {
+ method: "PATCH",
+ headers: {
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(values),
+ });
+
+ if (!response.ok) {
+ setPopup({ message: "Failed to update prompt!", type: "error" });
+ }
+
+ setNewPromptId(null);
+ refreshPrompts();
+ } catch (err) {
+ setPopup({ message: `Failed to update prompt: ${err}`, type: "error" });
+ }
+ };
+
+ if (isLoading) {
+ return ;
+ }
+
+ if (error || !promptLibrary) {
+ return (
+
+ );
+ }
+
+ const handleEdit = (promptId: number) => {
+ setNewPromptId(promptId);
+ };
+
+ return (
+
+ {popup}
+
+ {newPrompt && (
+
setNewPrompt(false)}
+ />
+ )}
+
+ {newPromptId && (
+ setNewPromptId(null)}
+ />
+ )}
+
+
+ Create prompts that can be accessed with the `/` shortcut in
+ Danswer Chat.{" "}
+ {isPublic
+ ? "Prompts created here will be accessible to all users."
+ : "Prompts created here will be available only to you."}
+
+
+
+
+
+ setNewPrompt(true)}
+ className={centering ? "mx-auto" : ""}
+ color="green"
+ size="xs"
+ >
+ New Prompt
+
+
+
+
+
+
+ );
+};
diff --git a/web/src/app/admin/settings/page.tsx b/web/src/app/admin/settings/page.tsx
index 1fe2ca883..9cb2f630e 100644
--- a/web/src/app/admin/settings/page.tsx
+++ b/web/src/app/admin/settings/page.tsx
@@ -1,5 +1,5 @@
import { AdminPageTitle } from "@/components/admin/Title";
-import { FiSettings } from "react-icons/fi";
+
import { SettingsForm } from "./SettingsForm";
import { Text } from "@tremor/react";
import { SettingsIcon } from "@/components/icons/icons";
diff --git a/web/src/app/assistants/SidebarWrapper.tsx b/web/src/app/assistants/SidebarWrapper.tsx
index 7f12d86ef..126a0a826 100644
--- a/web/src/app/assistants/SidebarWrapper.tsx
+++ b/web/src/app/assistants/SidebarWrapper.tsx
@@ -27,6 +27,7 @@ interface SidebarWrapperProps {
};
contentProps: T;
page: pageType;
+ size?: "sm" | "lg";
}
export default function SidebarWrapper({
@@ -38,6 +39,7 @@ export default function SidebarWrapper({
headerProps,
contentProps,
content,
+ size = "sm",
}: SidebarWrapperProps) {
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
@@ -140,7 +142,9 @@ export default function SidebarWrapper({
${toggledSidebar ? "w-[250px]" : "w-[0px]"}`}
/>
-
+
{content(contentProps)}
diff --git a/web/src/app/assistants/gallery/page.tsx b/web/src/app/assistants/gallery/page.tsx
index ee5d9f8d3..d5215f179 100644
--- a/web/src/app/assistants/gallery/page.tsx
+++ b/web/src/app/assistants/gallery/page.tsx
@@ -36,6 +36,7 @@ export default async function GalleryPage({
openedFolders,
shouldShowWelcomeModal,
toggleSidebar,
+ userInputPrompts,
} = data;
return (
@@ -55,6 +56,7 @@ export default async function GalleryPage({
llmProviders,
folders,
openedFolders,
+ userInputPrompts,
}}
>
(
+
+ )}
+ />
+ );
+}
diff --git a/web/src/app/assistants/mine/page.tsx b/web/src/app/assistants/mine/page.tsx
index afd580b95..e167a10a8 100644
--- a/web/src/app/assistants/mine/page.tsx
+++ b/web/src/app/assistants/mine/page.tsx
@@ -38,6 +38,7 @@ export default async function GalleryPage({
openedFolders,
shouldShowWelcomeModal,
toggleSidebar,
+ userInputPrompts,
} = data;
return (
@@ -57,6 +58,7 @@ export default async function GalleryPage({
llmProviders,
folders,
openedFolders,
+ userInputPrompts,
}}
>
+
New Assistant
diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx
index 7338c72e0..62610197e 100644
--- a/web/src/app/chat/ChatPage.tsx
+++ b/web/src/app/chat/ChatPage.tsx
@@ -102,6 +102,7 @@ export function ChatPage({
llmProviders,
folders,
openedFolders,
+ userInputPrompts,
} = useChatContext();
// chat session
@@ -1599,6 +1600,7 @@ export function ChatPage({
)}
setDocumentSelection(true)}
selectedDocuments={selectedDocuments}
// assistant stuff
diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx
index 56c15cf5c..5abd8c7fb 100644
--- a/web/src/app/chat/input/ChatInputBar.tsx
+++ b/web/src/app/chat/input/ChatInputBar.tsx
@@ -2,6 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
import { FiPlusCircle, FiPlus, FiInfo, FiX } from "react-icons/fi";
import { ChatInputOption } from "./ChatInputOption";
import { Persona } from "@/app/admin/assistants/interfaces";
+import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
import {
FilterManager,
getDisplayNameForModel,
@@ -55,12 +56,14 @@ export function ChatInputBar({
textAreaRef,
alternativeAssistant,
chatSessionId,
+ inputPrompts,
}: {
showDocs: () => void;
selectedDocuments: DanswerDocument[];
assistantOptions: Persona[];
setAlternativeAssistant: (alternativeAssistant: Persona | null) => void;
setSelectedAssistant: (assistant: Persona) => void;
+ inputPrompts: InputPrompt[];
message: string;
setMessage: (message: string) => void;
onSubmit: () => void;
@@ -76,7 +79,6 @@ export function ChatInputBar({
textAreaRef: React.RefObject;
chatSessionId?: number;
}) {
- // handle re-sizing of the text area
useEffect(() => {
const textarea = textAreaRef.current;
if (textarea) {
@@ -111,9 +113,28 @@ export function ChatInputBar({
const suggestionsRef = useRef(null);
const [showSuggestions, setShowSuggestions] = useState(false);
+ const [showPrompts, setShowPrompts] = useState(false);
const interactionsRef = useRef(null);
- // Click out of assistant suggestions
+
+ const hideSuggestions = () => {
+ setShowSuggestions(false);
+ setTabbingIconIndex(0);
+ };
+
+ const hidePrompts = () => {
+ setTimeout(() => {
+ setShowPrompts(false);
+ }, 50);
+
+ setTabbingIconIndex(0);
+ };
+
+ const updateInputPrompt = (prompt: InputPrompt) => {
+ hidePrompts();
+ setMessage(`${prompt.content}`);
+ };
+
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
@@ -123,6 +144,7 @@ export function ChatInputBar({
!interactionsRef.current.contains(event.target as Node))
) {
hideSuggestions();
+ hidePrompts();
}
};
document.addEventListener("mousedown", handleClickOutside);
@@ -131,12 +153,6 @@ export function ChatInputBar({
};
}, []);
- const hideSuggestions = () => {
- setShowSuggestions(false);
- setAssistantIconIndex(0);
- };
-
- // Update selected persona
const updatedTaggedAssistant = (assistant: Persona) => {
setAlternativeAssistant(
assistant.id == selectedAssistant.id ? null : assistant
@@ -145,22 +161,37 @@ export function ChatInputBar({
setMessage("");
};
- // Complete user input handling
+ const handleAssistantInput = (text: string) => {
+ if (!text.startsWith("@")) {
+ hideSuggestions();
+ } else {
+ const match = text.match(/(?:\s|^)@(\w*)$/);
+ if (match) {
+ setShowSuggestions(true);
+ } else {
+ hideSuggestions();
+ }
+ }
+ };
+
+ const handlePromptInput = (text: string) => {
+ if (!text.startsWith("/")) {
+ hidePrompts();
+ } else {
+ const promptMatch = text.match(/(?:\s|^)\/(\w*)$/);
+ if (promptMatch) {
+ setShowPrompts(true);
+ } else {
+ hidePrompts();
+ }
+ }
+ };
+
const handleInputChange = (event: React.ChangeEvent) => {
const text = event.target.value;
setMessage(text);
-
- if (!text.startsWith("@")) {
- hideSuggestions();
- return;
- }
-
- const match = text.match(/(?:\s|^)@(\w*)$/);
- if (match) {
- setShowSuggestions(true);
- } else {
- hideSuggestions();
- }
+ handleAssistantInput(text);
+ handlePromptInput(text);
};
const assistantTagOptions = assistantOptions.filter((assistant) =>
@@ -172,38 +203,65 @@ export function ChatInputBar({
)
);
- const [assistantIconIndex, setAssistantIconIndex] = useState(0);
+ const filteredPrompts = inputPrompts.filter(
+ (prompt) =>
+ prompt.active &&
+ prompt.prompt.toLowerCase().startsWith(
+ message
+ .slice(message.lastIndexOf("/") + 1)
+ .split(/\s/)[0]
+ .toLowerCase()
+ )
+ );
+
+ const [tabbingIconIndex, setTabbingIconIndex] = useState(0);
const handleKeyDown = (e: React.KeyboardEvent) => {
if (
- showSuggestions &&
- assistantTagOptions.length > 0 &&
+ ((showSuggestions && assistantTagOptions.length > 0) || showPrompts) &&
(e.key === "Tab" || e.key == "Enter")
) {
e.preventDefault();
- if (assistantIconIndex == assistantTagOptions.length) {
- window.open("/assistants/new", "_blank");
- hideSuggestions();
- setMessage("");
+
+ if (
+ (tabbingIconIndex == assistantTagOptions.length && showSuggestions) ||
+ (tabbingIconIndex == filteredPrompts.length && showPrompts)
+ ) {
+ if (showPrompts) {
+ window.open("/prompts", "_self");
+ } else {
+ window.open("/assistants/new", "_self");
+ }
} else {
- const option =
- assistantTagOptions[assistantIconIndex >= 0 ? assistantIconIndex : 0];
- updatedTaggedAssistant(option);
+ if (showPrompts) {
+ const uppity =
+ filteredPrompts[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
+ updateInputPrompt(uppity);
+ } else {
+ const option =
+ assistantTagOptions[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
+
+ updatedTaggedAssistant(option);
+ }
}
}
- if (!showSuggestions) {
+ if (!showPrompts && !showSuggestions) {
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
- setAssistantIconIndex((assistantIconIndex) =>
- Math.min(assistantIconIndex + 1, assistantTagOptions.length)
+
+ setTabbingIconIndex((tabbingIconIndex) =>
+ Math.min(
+ tabbingIconIndex + 1,
+ showPrompts ? filteredPrompts.length : assistantTagOptions.length
+ )
);
} else if (e.key === "ArrowUp") {
e.preventDefault();
- setAssistantIconIndex((assistantIconIndex) =>
- Math.max(assistantIconIndex - 1, 0)
+ setTabbingIconIndex((tabbingIconIndex) =>
+ Math.max(tabbingIconIndex - 1, 0)
);
}
};
@@ -231,7 +289,7 @@ export function ChatInputBar({
{
updatedTaggedAssistant(currentAssistant);
@@ -245,12 +303,12 @@ export function ChatInputBar({
))}
+
@@ -260,6 +318,42 @@ export function ChatInputBar({
)}
+
+ {showPrompts && (
+
+
+ {filteredPrompts.map((currentPrompt, index) => (
+
{
+ updateInputPrompt(currentPrompt);
+ }}
+ >
+ {currentPrompt.prompt}
+
+ {currentPrompt.id == selectedAssistant.id && "(default) "}
+ {currentPrompt.content}
+
+
+ ))}
+
+
+
+ Create a new prompt
+
+
+
+ )}
+
@@ -391,11 +485,13 @@ export function ChatInputBar({
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
- placeholder={`Send a message ${!settings?.isMobile ? "or @ to tag an assistant..." : ""}`}
+ placeholder={`Send a message ${!settings?.isMobile ? "or try using @ or /" : ""}`}
value={message}
onKeyDown={(event) => {
if (
event.key === "Enter" &&
+ !showPrompts &&
+ !showSuggestions &&
!event.shiftKey &&
message &&
!isStreaming
@@ -453,7 +549,6 @@ export function ChatInputBar({
/>
)}
position="top"
- // flexPriority="second"
>
+ {onClose && (
+
+
+
+
+
+ )}
+
{children}
diff --git a/web/src/app/chat/page.tsx b/web/src/app/chat/page.tsx
index f518a0044..48c52dbf2 100644
--- a/web/src/app/chat/page.tsx
+++ b/web/src/app/chat/page.tsx
@@ -39,6 +39,7 @@ export default async function Page({
finalDocumentSidebarInitialWidth,
shouldShowWelcomeModal,
shouldDisplaySourcesIncompleteModal,
+ userInputPrompts,
} = data;
return (
@@ -59,6 +60,7 @@ export default async function Page({
llmProviders,
folders,
openedFolders,
+ userInputPrompts,
}}
>
(
Manage Assistants
+
+
+
+ Manage Prompts
+
+
)}
diff --git a/web/src/app/prompts/page.tsx b/web/src/app/prompts/page.tsx
new file mode 100644
index 000000000..8bf104d5b
--- /dev/null
+++ b/web/src/app/prompts/page.tsx
@@ -0,0 +1,40 @@
+import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
+import { fetchChatData } from "@/lib/chat/fetchChatData";
+import { unstable_noStore as noStore } from "next/cache";
+import { redirect } from "next/navigation";
+import WrappedPrompts from "../assistants/mine/WrappedInputPrompts";
+
+export default async function GalleryPage({
+ searchParams,
+}: {
+ searchParams: { [key: string]: string };
+}) {
+ noStore();
+
+ const data = await fetchChatData(searchParams);
+
+ if ("redirect" in data) {
+ redirect(data.redirect);
+ }
+
+ const {
+ user,
+ chatSessions,
+ assistants,
+ folders,
+ openedFolders,
+ shouldShowWelcomeModal,
+ toggleSidebar,
+ } = data;
+
+ return (
+
+ );
+}
diff --git a/web/src/components/admin/ClientLayout.tsx b/web/src/components/admin/ClientLayout.tsx
index 622f10025..15f723b65 100644
--- a/web/src/components/admin/ClientLayout.tsx
+++ b/web/src/components/admin/ClientLayout.tsx
@@ -20,12 +20,14 @@ import {
DocumentSetIconSkeleton,
EmbeddingIconSkeleton,
AssistantsIconSkeleton,
+ ClosedBookIcon,
} from "@/components/icons/icons";
import { FiActivity, FiBarChart2 } from "react-icons/fi";
import { UserDropdown } from "../UserDropdown";
import { User } from "@/lib/types";
import { usePathname } from "next/navigation";
+import { PencilCircle } from "@phosphor-icons/react";
export function ClientLayout({
user,
@@ -144,6 +146,15 @@ export function ClientLayout({
),
link: "/admin/standard-answer",
},
+ {
+ name: (
+
+ ),
+ link: "/admin/prompt-library",
+ },
],
},
{
diff --git a/web/src/components/admin/Layout.tsx b/web/src/components/admin/Layout.tsx
index 2450d21a3..b2f08ade4 100644
--- a/web/src/components/admin/Layout.tsx
+++ b/web/src/components/admin/Layout.tsx
@@ -1,3 +1,19 @@
+import { Header } from "@/components/header/Header";
+import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
+import {
+ NotebookIcon,
+ UsersIcon,
+ ThumbsUpIcon,
+ BookmarkIcon,
+ ZoomInIcon,
+ RobotIcon,
+ ConnectorIcon,
+ GroupsIcon,
+ DatabaseIcon,
+ KeyIcon,
+ ClipboardIcon,
+ BookstackIcon,
+} from "@/components/icons/icons";
import { User } from "@/lib/types";
import {
AuthTypeMetadata,
diff --git a/web/src/components/context/ChatContext.tsx b/web/src/components/context/ChatContext.tsx
index 2c114c5ce..74772cc29 100644
--- a/web/src/components/context/ChatContext.tsx
+++ b/web/src/components/context/ChatContext.tsx
@@ -12,6 +12,7 @@ import { ChatSession } from "@/app/chat/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { LLMProviderDescriptor } from "@/app/admin/models/llm/interfaces";
import { Folder } from "@/app/chat/folders/interfaces";
+import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
interface ChatContextProps {
user: User | null;
@@ -23,6 +24,7 @@ interface ChatContextProps {
llmProviders: LLMProviderDescriptor[];
folders: Folder[];
openedFolders: Record;
+ userInputPrompts: InputPrompt[];
}
const ChatContext = createContext(undefined);
diff --git a/web/src/components/icons/icons.tsx b/web/src/components/icons/icons.tsx
index e6f1b74a1..ec9e381c1 100644
--- a/web/src/components/icons/icons.tsx
+++ b/web/src/components/icons/icons.tsx
@@ -2366,3 +2366,27 @@ export const SwapIcon = ({
);
};
+
+export const ClosedBookIcon = ({
+ size = 16,
+ className = defaultTailwindCSS,
+}: IconProps) => {
+ return (
+
+
+
+ );
+};
diff --git a/web/src/components/search/filtering/FilterDropdown.tsx b/web/src/components/search/filtering/FilterDropdown.tsx
index bee6a3962..1c3028d60 100644
--- a/web/src/components/search/filtering/FilterDropdown.tsx
+++ b/web/src/components/search/filtering/FilterDropdown.tsx
@@ -82,11 +82,12 @@ export function FilterDropdown({
py-1.5
rounded-lg
border
+ gap-x-2
border-border
cursor-pointer
hover:bg-hover-light`}
>
- {icon}
+ {icon}
{selected.length === 0 ? (
defaultDisplay
) : (
diff --git a/web/src/lib/chat/fetchChatData.ts b/web/src/lib/chat/fetchChatData.ts
index fb19037a9..f59850812 100644
--- a/web/src/lib/chat/fetchChatData.ts
+++ b/web/src/lib/chat/fetchChatData.ts
@@ -13,6 +13,7 @@ import {
} from "@/lib/types";
import { ChatSession } from "@/app/chat/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
+import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
import { FullEmbeddingModelResponse } from "@/app/admin/models/embedding/components/types";
import { Settings } from "@/app/admin/settings/interfaces";
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
@@ -44,6 +45,7 @@ interface FetchChatDataResult {
finalDocumentSidebarInitialWidth?: number;
shouldShowWelcomeModal: boolean;
shouldDisplaySourcesIncompleteModal: boolean;
+ userInputPrompts: InputPrompt[];
}
export async function fetchChatData(searchParams: {
@@ -59,6 +61,7 @@ export async function fetchChatData(searchParams: {
fetchSS("/query/valid-tags"),
fetchLLMProvidersSS(),
fetchSS("/folder"),
+ fetchSS("/input_prompt?include_public=true"),
];
let results: (
@@ -90,8 +93,8 @@ export async function fetchChatData(searchParams: {
const tagsResponse = results[6] as Response | null;
const llmProviders = (results[7] || []) as LLMProviderDescriptor[];
-
- const foldersResponse = results[8] as Response | null; // Handle folders result
+ const foldersResponse = results[8] as Response | null;
+ const userInputPromptsResponse = results[9] as Response | null;
const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) {
@@ -135,6 +138,15 @@ export async function fetchChatData(searchParams: {
);
}
+ let userInputPrompts: InputPrompt[] = [];
+ if (userInputPromptsResponse?.ok) {
+ userInputPrompts = await userInputPromptsResponse.json();
+ } else {
+ console.log(
+ `Failed to fetch user input prompts - ${userInputPromptsResponse?.status}`
+ );
+ }
+
let assistants = rawAssistantsList;
if (assistantsFetchError) {
console.log(`Failed to fetch assistants - ${assistantsFetchError}`);
@@ -218,5 +230,6 @@ export async function fetchChatData(searchParams: {
toggleSidebar,
shouldShowWelcomeModal,
shouldDisplaySourcesIncompleteModal,
+ userInputPrompts,
};
}