mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-04-07 19:38:19 +02:00
Add Prompt library (#1990)
This commit is contained in:
parent
445f7e70ba
commit
459bd46846
58
backend/alembic/versions/e1392f05e840_added_input_prompts.py
Normal file
58
backend/alembic/versions/e1392f05e840_added_input_prompts.py
Normal file
@ -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")
|
@ -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
|
||||
|
24
backend/danswer/chat/input_prompts.yaml
Normal file
24
backend/danswer/chat/input_prompts.yaml
Normal file
@ -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
|
@ -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)
|
||||
|
@ -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
|
||||
|
202
backend/danswer/db/input_prompt.py
Normal file
202
backend/danswer/db/input_prompt.py
Normal file
@ -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())
|
@ -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
|
||||
|
||||
|
@ -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)
|
||||
|
134
backend/danswer/server/features/input_prompt/api.py
Normal file
134
backend/danswer/server/features/input_prompt/api.py
Normal file
@ -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]
|
47
backend/danswer/server/features/input_prompt/models.py
Normal file
47
backend/danswer/server/features/input_prompt/models.py
Normal file
@ -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,
|
||||
)
|
@ -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,
|
||||
|
46
web/src/app/admin/prompt-library/hooks.ts
Normal file
46
web/src/app/admin/prompt-library/hooks.ts
Normal file
@ -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<InputPrompt[]>(
|
||||
`/api/admin/input_prompt`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading: !error && !data,
|
||||
refreshInputPrompts: mutate,
|
||||
};
|
||||
};
|
||||
|
||||
export const useInputPrompts = (includePublic: boolean = false) => {
|
||||
const { data, error, mutate } = useSWR<InputPrompt[]>(
|
||||
`/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<InputPrompt>(
|
||||
`/api/input_prompt/${id}`,
|
||||
fetcher
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
error,
|
||||
isLoading: !error && !data,
|
||||
refreshInputPrompt: mutate,
|
||||
};
|
||||
};
|
31
web/src/app/admin/prompt-library/interfaces.ts
Normal file
31
web/src/app/admin/prompt-library/interfaces.ts
Normal file
@ -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<void>;
|
||||
}
|
||||
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;
|
||||
}
|
92
web/src/app/admin/prompt-library/modals/AddPromptModal.tsx
Normal file
92
web/src/app/admin/prompt-library/modals/AddPromptModal.tsx
Normal file
@ -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 (
|
||||
<ModalWrapper onClose={onClose} modalClassName="max-w-xl">
|
||||
<Formik
|
||||
initialValues={{
|
||||
title: "",
|
||||
prompt: "",
|
||||
}}
|
||||
validationSchema={AddPromptSchema}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
onSubmit({
|
||||
prompt: values.title,
|
||||
content: values.prompt,
|
||||
});
|
||||
setSubmitting(false);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<h2 className="text-2xl gap-x-2 text-emphasis font-bold mb-3 flex items-center">
|
||||
<BookstackIcon size={20} />
|
||||
Add prompt
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<TextFormField
|
||||
label="Title"
|
||||
name="title"
|
||||
placeholder="Title (e.g. 'Reword')"
|
||||
/>
|
||||
|
||||
<TextFormField
|
||||
isTextArea
|
||||
label="Prompt"
|
||||
name="prompt"
|
||||
placeholder="Enter a prompt (e.g. 'help me rewrite the following politely and concisely for professional communication')"
|
||||
/>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
>
|
||||
Add prompt
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPromptModal;
|
138
web/src/app/admin/prompt-library/modals/EditPromptModal.tsx
Normal file
138
web/src/app/admin/prompt-library/modals/EditPromptModal.tsx
Normal file
@ -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 (
|
||||
<ModalWrapper onClose={onClose} modalClassName="max-w-xl">
|
||||
<p>Failed to load prompt data</p>
|
||||
</ModalWrapper>
|
||||
);
|
||||
|
||||
if (!promptData)
|
||||
return (
|
||||
<ModalWrapper onClose={onClose} modalClassName="max-w-xl">
|
||||
<p>Loading...</p>
|
||||
</ModalWrapper>
|
||||
);
|
||||
|
||||
return (
|
||||
<ModalWrapper onClose={onClose} modalClassName="max-w-xl">
|
||||
<Formik
|
||||
initialValues={{
|
||||
prompt: promptData.prompt,
|
||||
content: promptData.content,
|
||||
active: promptData.active,
|
||||
}}
|
||||
validationSchema={EditPromptSchema}
|
||||
onSubmit={(values) => {
|
||||
editInputPrompt(promptId, values);
|
||||
refreshInputPrompt();
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, values }) => (
|
||||
<Form>
|
||||
<h2 className="text-2xl text-emphasis font-bold mb-3 flex items-center">
|
||||
<svg
|
||||
className="w-6 h-6 mr-2"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
>
|
||||
<path d="M3 17.25V21h3.75L17.81 9.94l-3.75-3.75L3 17.25zM20.71 7.04c.39-.39.39-1.02 0-1.41l-2.34-2.34c-.39-.39-1.02-.39-1.41 0l-1.83 1.83 3.75 3.75 1.83-1.83z" />
|
||||
</svg>
|
||||
Edit prompt
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="prompt"
|
||||
className="block text-sm font-medium mb-1"
|
||||
>
|
||||
Title
|
||||
</label>
|
||||
<Field
|
||||
as={TextInput}
|
||||
id="prompt"
|
||||
name="prompt"
|
||||
placeholder="Title (e.g. 'Draft email')"
|
||||
/>
|
||||
<ErrorMessage
|
||||
name="prompt"
|
||||
component="div"
|
||||
className="text-red-500 text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label
|
||||
htmlFor="content"
|
||||
className="block text-sm font-medium mb-1"
|
||||
>
|
||||
Content
|
||||
</label>
|
||||
<Field
|
||||
as={Textarea}
|
||||
id="content"
|
||||
name="content"
|
||||
placeholder="Enter prompt content (e.g. 'Write a professional-sounding email about the following content')"
|
||||
rows={4}
|
||||
/>
|
||||
<ErrorMessage
|
||||
name="content"
|
||||
component="div"
|
||||
className="text-red-500 text-sm mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="flex items-center">
|
||||
<Field type="checkbox" name="active" className="mr-2" />
|
||||
Active prompt
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6">
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
(values.prompt === promptData.prompt &&
|
||||
values.content === promptData.content &&
|
||||
values.active === promptData.active)
|
||||
}
|
||||
>
|
||||
{isSubmitting ? "Updating..." : "Update prompt"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</ModalWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPromptModal;
|
32
web/src/app/admin/prompt-library/page.tsx
Normal file
32
web/src/app/admin/prompt-library/page.tsx
Normal file
@ -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 (
|
||||
<div className="container mx-auto">
|
||||
<AdminPageTitle
|
||||
icon={<ClosedBookIcon size={32} />}
|
||||
title="Prompt Library"
|
||||
/>
|
||||
<PromptSection
|
||||
promptLibrary={promptLibrary || []}
|
||||
isLoading={promptLibraryIsLoading}
|
||||
error={promptLibraryError}
|
||||
refreshPrompts={refreshPrompts}
|
||||
isPublic={true}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
export default Page;
|
260
web/src/app/admin/prompt-library/promptLibrary.tsx
Normal file
260
web/src/app/admin/prompt-library/promptLibrary.tsx
Normal file
@ -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;
|
||||
}) => (
|
||||
<span
|
||||
className={`
|
||||
inline-block
|
||||
px-2
|
||||
py-1
|
||||
mr-1
|
||||
mb-1
|
||||
text-xs
|
||||
font-semibold
|
||||
text-emphasis
|
||||
bg-hover
|
||||
rounded-full
|
||||
items-center
|
||||
w-fit
|
||||
${onDelete ? "cursor-pointer" : ""}
|
||||
`}
|
||||
onClick={onDelete}
|
||||
>
|
||||
{name}
|
||||
{onDelete && (
|
||||
<button
|
||||
className="ml-1 text-subtle hover:text-emphasis"
|
||||
aria-label="Remove category"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
|
||||
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<string[]>([]);
|
||||
|
||||
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<number | null>(
|
||||
null
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="justify-center py-2">
|
||||
{confirmDeletionId != null && (
|
||||
<Modal
|
||||
onOutsideClick={() => setConfirmDeletionId(null)}
|
||||
className="max-w-sm"
|
||||
>
|
||||
<>
|
||||
<p className="text-lg mb-2">
|
||||
Are you sure you want to delete this prompt? You will not be able
|
||||
to recover this prompt
|
||||
</p>
|
||||
<div className="mt-6 flex justify-between">
|
||||
<button
|
||||
className="rounded py-1.5 px-2 bg-background-800 text-text-200"
|
||||
onClick={async () => {
|
||||
await handleDelete(confirmDeletionId);
|
||||
setConfirmDeletionId(null);
|
||||
}}
|
||||
>
|
||||
Yes
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setConfirmDeletionId(null)}
|
||||
className="rounded py-1.5 px-2 bg-background-150 text-text-800"
|
||||
>
|
||||
{" "}
|
||||
No
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent">
|
||||
<MagnifyingGlass />
|
||||
<input
|
||||
className="flex-grow ml-2 bg-transparent outline-none placeholder-subtle"
|
||||
placeholder="Find prompts..."
|
||||
value={query}
|
||||
onChange={(event) => {
|
||||
setQuery(event.target.value);
|
||||
setCurrentPage(1);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<div className="my-4 border-b border-border">
|
||||
<FilterDropdown
|
||||
options={[
|
||||
{ key: "Active", display: "Active" },
|
||||
{ key: "Inactive", display: "Inactive" },
|
||||
]}
|
||||
selected={selectedStatus}
|
||||
handleSelect={(option) => handleStatusSelect(option.key)}
|
||||
icon={<FiTag size={16} />}
|
||||
defaultDisplay="All Statuses"
|
||||
/>
|
||||
<div className="flex flex-wrap pb-4 mt-3">
|
||||
{selectedStatus.map((status) => (
|
||||
<CategoryBubble
|
||||
key={status}
|
||||
name={status}
|
||||
onDelete={() => handleStatusSelect(status)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto">
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHeaderCell key={column.key}>
|
||||
{column.name}
|
||||
</TableHeaderCell>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{paginatedPromptLibrary.length > 0 ? (
|
||||
paginatedPromptLibrary
|
||||
.filter((prompt) => !(!isPublic && prompt.is_public))
|
||||
.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.prompt}</TableCell>
|
||||
<TableCell>{item.content}</TableCell>
|
||||
<TableCell>{item.active ? "Active" : "Inactive"}</TableCell>
|
||||
<TableCell>
|
||||
<button
|
||||
className="cursor-pointer"
|
||||
onClick={() => setConfirmDeletionId(item.id)}
|
||||
>
|
||||
<TrashIcon size={20} />
|
||||
</button>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<button onClick={() => handleEdit(item.id)}>
|
||||
<EditIcon size={12} />
|
||||
</button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6}>No matching prompts found...</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
{paginatedPromptLibrary.length > 0 && (
|
||||
<div className="mt-4 flex justify-center">
|
||||
<PageSelector
|
||||
currentPage={currentPage}
|
||||
totalPages={totalPages}
|
||||
onPageChange={handlePageChange}
|
||||
shouldScroll={true}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
146
web/src/app/admin/prompt-library/promptSection.tsx
Normal file
146
web/src/app/admin/prompt-library/promptSection.tsx
Normal file
@ -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<number | null>(null);
|
||||
|
||||
const createInputPrompt = async (
|
||||
promptData: CreateInputPromptRequest
|
||||
): Promise<InputPrompt> => {
|
||||
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 <ThreeDotsLoader />;
|
||||
}
|
||||
|
||||
if (error || !promptLibrary) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Error loading standard answers"
|
||||
errorMsg={error?.info?.message || error?.message?.info?.detail}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const handleEdit = (promptId: number) => {
|
||||
setNewPromptId(promptId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`w-full ${centering ? "flex-col flex justify-center" : ""} mb-8`}
|
||||
>
|
||||
{popup}
|
||||
|
||||
{newPrompt && (
|
||||
<AddPromptModal
|
||||
onSubmit={createInputPrompt}
|
||||
onClose={() => setNewPrompt(false)}
|
||||
/>
|
||||
)}
|
||||
|
||||
{newPromptId && (
|
||||
<EditPromptModal
|
||||
promptId={newPromptId}
|
||||
editInputPrompt={editInputPrompt}
|
||||
onClose={() => setNewPromptId(null)}
|
||||
/>
|
||||
)}
|
||||
<div className={centering ? "max-w-sm mx-auto" : ""}>
|
||||
<Text className="mb-2 my-auto">
|
||||
Create prompts that can be accessed with the <i>`/`</i> shortcut in
|
||||
Danswer Chat.{" "}
|
||||
{isPublic
|
||||
? "Prompts created here will be accessible to all users."
|
||||
: "Prompts created here will be available only to you."}
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="mb-2"></div>
|
||||
|
||||
<Button
|
||||
onClick={() => setNewPrompt(true)}
|
||||
className={centering ? "mx-auto" : ""}
|
||||
color="green"
|
||||
size="xs"
|
||||
>
|
||||
New Prompt
|
||||
</Button>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div>
|
||||
<PromptLibraryTable
|
||||
isPublic={isPublic}
|
||||
promptLibrary={promptLibrary}
|
||||
setPopup={setPopup}
|
||||
refresh={refreshPrompts}
|
||||
handleEdit={handleEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@ -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";
|
||||
|
@ -27,6 +27,7 @@ interface SidebarWrapperProps<T extends object> {
|
||||
};
|
||||
contentProps: T;
|
||||
page: pageType;
|
||||
size?: "sm" | "lg";
|
||||
}
|
||||
|
||||
export default function SidebarWrapper<T extends object>({
|
||||
@ -38,6 +39,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
headerProps,
|
||||
contentProps,
|
||||
content,
|
||||
size = "sm",
|
||||
}: SidebarWrapperProps<T>) {
|
||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||
|
||||
@ -140,7 +142,9 @@ export default function SidebarWrapper<T extends object>({
|
||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}`}
|
||||
/>
|
||||
|
||||
<div className="mt-4 w-full max-w-3xl mx-auto">
|
||||
<div
|
||||
className={`mt-4 w-full ${size == "lg" ? "max-w-4xl" : "max-w-3xl"} mx-auto`}
|
||||
>
|
||||
{content(contentProps)}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,
|
||||
}}
|
||||
>
|
||||
<WrappedAssistantsGallery
|
||||
|
64
web/src/app/assistants/mine/WrappedInputPrompts.tsx
Normal file
64
web/src/app/assistants/mine/WrappedInputPrompts.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
"use client";
|
||||
import SidebarWrapper from "../SidebarWrapper";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
import { AssistantsPageTitle } from "../AssistantsPageTitle";
|
||||
import { useInputPrompts } from "@/app/admin/prompt-library/hooks";
|
||||
import { PromptSection } from "@/app/admin/prompt-library/promptSection";
|
||||
|
||||
export default function WrappedPrompts({
|
||||
chatSessions,
|
||||
initiallyToggled,
|
||||
folders,
|
||||
openedFolders,
|
||||
user,
|
||||
assistants,
|
||||
}: {
|
||||
chatSessions: ChatSession[];
|
||||
folders: Folder[];
|
||||
initiallyToggled: boolean;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
user: User | null;
|
||||
assistants: Persona[];
|
||||
}) {
|
||||
const {
|
||||
data: promptLibrary,
|
||||
error: promptLibraryError,
|
||||
isLoading: promptLibraryIsLoading,
|
||||
refreshInputPrompts: refreshPrompts,
|
||||
} = useInputPrompts(false);
|
||||
|
||||
return (
|
||||
<SidebarWrapper
|
||||
size="lg"
|
||||
page="chat"
|
||||
initiallyToggled={initiallyToggled}
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
headerProps={{ user, page: "chat" }}
|
||||
contentProps={{
|
||||
assistants: assistants,
|
||||
user: user,
|
||||
}}
|
||||
content={(contentProps) => (
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Prompt Gallery</AssistantsPageTitle>
|
||||
<InstantSSRAutoRefresh />
|
||||
<PromptSection
|
||||
promptLibrary={promptLibrary || []}
|
||||
isLoading={promptLibraryIsLoading}
|
||||
error={promptLibraryError}
|
||||
refreshPrompts={refreshPrompts}
|
||||
isPublic={false}
|
||||
centering
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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,
|
||||
}}
|
||||
>
|
||||
<WrappedAssistantsMine
|
||||
|
@ -41,6 +41,7 @@ export default async function Page() {
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex my-auto">
|
||||
<LargeBackButton />
|
||||
|
||||
<h1 className="flex text-xl text-strong font-bold my-auto">
|
||||
New Assistant
|
||||
</h1>
|
||||
|
@ -102,6 +102,7 @@ export function ChatPage({
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
userInputPrompts,
|
||||
} = useChatContext();
|
||||
|
||||
// chat session
|
||||
@ -1599,6 +1600,7 @@ export function ChatPage({
|
||||
)}
|
||||
|
||||
<ChatInputBar
|
||||
inputPrompts={userInputPrompts}
|
||||
showDocs={() => setDocumentSelection(true)}
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
|
@ -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<HTMLTextAreaElement>;
|
||||
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<HTMLDivElement | null>(null);
|
||||
const [showSuggestions, setShowSuggestions] = useState(false);
|
||||
const [showPrompts, setShowPrompts] = useState(false);
|
||||
|
||||
const interactionsRef = useRef<HTMLDivElement | null>(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<HTMLTextAreaElement>) => {
|
||||
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<HTMLTextAreaElement>) => {
|
||||
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({
|
||||
<button
|
||||
key={index}
|
||||
className={`px-2 ${
|
||||
assistantIconIndex == index && "bg-hover-lightish"
|
||||
tabbingIconIndex == index && "bg-hover-lightish"
|
||||
} rounded rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-hover-lightish cursor-pointer`}
|
||||
onClick={() => {
|
||||
updatedTaggedAssistant(currentAssistant);
|
||||
@ -245,12 +303,12 @@ export function ChatInputBar({
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<a
|
||||
key={assistantTagOptions.length}
|
||||
target="_blank"
|
||||
target="_self"
|
||||
className={`${
|
||||
assistantIconIndex == assistantTagOptions.length &&
|
||||
"bg-hover"
|
||||
tabbingIconIndex == assistantTagOptions.length && "bg-hover"
|
||||
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-hover-lightish cursor-pointer"`}
|
||||
href="/assistants/new"
|
||||
>
|
||||
@ -260,6 +318,42 @@ export function ChatInputBar({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{showPrompts && (
|
||||
<div
|
||||
ref={suggestionsRef}
|
||||
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
|
||||
>
|
||||
<div className="rounded-lg py-1.5 bg-white border border-border-medium overflow-hidden shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
|
||||
{filteredPrompts.map((currentPrompt, index) => (
|
||||
<button
|
||||
key={index}
|
||||
className={`px-2 ${tabbingIconIndex == index && "bg-hover"} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-hover cursor-pointer`}
|
||||
onClick={() => {
|
||||
updateInputPrompt(currentPrompt);
|
||||
}}
|
||||
>
|
||||
<p className="font-bold ">{currentPrompt.prompt}</p>
|
||||
<p className="line-clamp-1">
|
||||
{currentPrompt.id == selectedAssistant.id && "(default) "}
|
||||
{currentPrompt.content}
|
||||
</p>
|
||||
</button>
|
||||
))}
|
||||
|
||||
<a
|
||||
key={filteredPrompts.length}
|
||||
target="_self"
|
||||
className={`${tabbingIconIndex == filteredPrompts.length && "bg-hover"} px-3 flex gap-x-1 py-2 w-full items-center hover:bg-hover-light cursor-pointer"`}
|
||||
href="/prompts"
|
||||
>
|
||||
<FiPlus size={17} />
|
||||
<p>Create a new prompt</p>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<SelectedFilterDisplay filterManager={filterManager} />
|
||||
</div>
|
||||
@ -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"
|
||||
>
|
||||
<ChatInputOption
|
||||
flexPriority="second"
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { XIcon } from "@/components/icons/icons";
|
||||
|
||||
export const ModalWrapper = ({
|
||||
children,
|
||||
bgClassName,
|
||||
@ -29,6 +31,14 @@ export const ModalWrapper = ({
|
||||
(modalClassName || "")
|
||||
}
|
||||
>
|
||||
{onClose && (
|
||||
<div className="w-full cursor-pointer flex justify-end">
|
||||
<button onClick={onClose}>
|
||||
<XIcon />
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -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,
|
||||
}}
|
||||
>
|
||||
<WrappedChat
|
||||
|
@ -32,6 +32,9 @@ import {
|
||||
AssistantsIcon,
|
||||
AssistantsIconSkeleton,
|
||||
BackIcon,
|
||||
BookIcon,
|
||||
BookmarkIconSkeleton,
|
||||
ClosedBookIcon,
|
||||
LefToLineIcon,
|
||||
RightToLineIcon,
|
||||
} from "@/components/icons/icons";
|
||||
@ -184,6 +187,15 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
Manage Assistants
|
||||
</p>
|
||||
</Link>
|
||||
<Link
|
||||
href="/prompts"
|
||||
className="w-full p-2 bg-white border-border border rounded items-center hover:bg-background-200 cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
>
|
||||
<ClosedBookIcon className="h-4 w-4 my-auto" />
|
||||
<p className="my-auto flex items-center text-sm">
|
||||
Manage Prompts
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-b border-border pb-4 mx-3" />
|
||||
|
40
web/src/app/prompts/page.tsx
Normal file
40
web/src/app/prompts/page.tsx
Normal file
@ -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 (
|
||||
<WrappedPrompts
|
||||
initiallyToggled={toggleSidebar}
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
user={user}
|
||||
assistants={assistants}
|
||||
/>
|
||||
);
|
||||
}
|
@ -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: (
|
||||
<div className="flex">
|
||||
<ClosedBookIcon size={18} />
|
||||
<div className="ml-1">Prompt Library</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/prompt-library",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@ -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,
|
||||
|
@ -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<string, boolean>;
|
||||
userInputPrompts: InputPrompt[];
|
||||
}
|
||||
|
||||
const ChatContext = createContext<ChatContextProps | undefined>(undefined);
|
||||
|
@ -2366,3 +2366,27 @@ export const SwapIcon = ({
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
||||
export const ClosedBookIcon = ({
|
||||
size = 16,
|
||||
className = defaultTailwindCSS,
|
||||
}: IconProps) => {
|
||||
return (
|
||||
<svg
|
||||
style={{ width: `${size}px`, height: `${size}px` }}
|
||||
className={`w-[${size}px] h-[${size}px] ` + className}
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="200"
|
||||
height="200"
|
||||
viewBox="0 0 14 14"
|
||||
>
|
||||
<path
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
d="M12.5 13.54H3a1.5 1.5 0 0 1 0-3h8.5a1 1 0 0 0 1-1v-8a1 1 0 0 0-1-1H3A1.5 1.5 0 0 0 1.5 2v10m10-1.46v3"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
};
|
||||
|
@ -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}
|
||||
<div className="flex-none my-auto">{icon}</div>
|
||||
{selected.length === 0 ? (
|
||||
defaultDisplay
|
||||
) : (
|
||||
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user