mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-10-11 05:36:03 +02:00
delete input prompts (#3380)
* delete input prompts * nit * remove vestigial test * nit
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
"""delete_input_prompts
|
||||
|
||||
Revision ID: bf7a81109301
|
||||
Revises: f7a894b06d02
|
||||
Create Date: 2024-12-09 12:00:49.884228
|
||||
|
||||
"""
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
import fastapi_users_db_sqlalchemy
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "bf7a81109301"
|
||||
down_revision = "f7a894b06d02"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
op.drop_table("inputprompt__user")
|
||||
op.drop_table("inputprompt")
|
||||
|
||||
|
||||
def downgrade() -> 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"),
|
||||
)
|
@@ -3,7 +3,6 @@ import os
|
||||
|
||||
PROMPTS_YAML = "./danswer/seeding/prompts.yaml"
|
||||
PERSONAS_YAML = "./danswer/seeding/personas.yaml"
|
||||
INPUT_PROMPT_YAML = "./danswer/seeding/input_prompts.yaml"
|
||||
|
||||
NUM_RETURNED_HITS = 50
|
||||
# Used for LLM filtering and reranking
|
||||
|
@@ -1,202 +0,0 @@
|
||||
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())
|
@@ -159,9 +159,6 @@ 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")
|
||||
@@ -178,31 +175,6 @@ class User(SQLAlchemyBaseUserTableUUID, Base):
|
||||
)
|
||||
|
||||
|
||||
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 | None] = mapped_column(
|
||||
ForeignKey("user.id", ondelete="CASCADE"), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class InputPrompt__User(Base):
|
||||
__tablename__ = "inputprompt__user"
|
||||
|
||||
input_prompt_id: Mapped[int] = mapped_column(
|
||||
ForeignKey("inputprompt.id"), primary_key=True
|
||||
)
|
||||
user_id: Mapped[UUID | None] = mapped_column(
|
||||
ForeignKey("inputprompt.id"), primary_key=True
|
||||
)
|
||||
|
||||
|
||||
class AccessToken(SQLAlchemyBaseAccessTokenTableUUID, Base):
|
||||
pass
|
||||
|
||||
|
@@ -54,10 +54,6 @@ from danswer.server.documents.document import router as document_router
|
||||
from danswer.server.documents.indexing import router as indexing_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.notifications.api import router as notification_router
|
||||
from danswer.server.features.persona.api import admin_router as admin_persona_router
|
||||
from danswer.server.features.persona.api import basic_router as persona_router
|
||||
@@ -258,8 +254,6 @@ def get_application() -> FastAPI:
|
||||
)
|
||||
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, notification_router)
|
||||
include_router_with_global_prefix_prepended(application, prompt_router)
|
||||
include_router_with_global_prefix_prepended(application, tool_router)
|
||||
|
@@ -1,24 +0,0 @@
|
||||
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,13 +1,11 @@
|
||||
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.context.search.enums import RecencyBiasSetting
|
||||
from danswer.db.document_set import get_or_create_document_set_by_name
|
||||
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 Persona
|
||||
from danswer.db.models import Prompt as PromptDBModel
|
||||
@@ -140,35 +138,10 @@ def load_personas_from_yaml(
|
||||
)
|
||||
|
||||
|
||||
def load_input_prompts_from_yaml(
|
||||
db_session: Session, 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", [])
|
||||
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(
|
||||
db_session: Session,
|
||||
prompt_yaml: str = PROMPTS_YAML,
|
||||
personas_yaml: str = PERSONAS_YAML,
|
||||
input_prompts_yaml: str = INPUT_PROMPT_YAML,
|
||||
) -> None:
|
||||
load_prompts_from_yaml(db_session, prompt_yaml)
|
||||
load_personas_from_yaml(db_session, personas_yaml)
|
||||
load_input_prompts_from_yaml(db_session, input_prompts_yaml)
|
||||
|
@@ -1,134 +0,0 @@
|
||||
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]
|
@@ -1,47 +0,0 @@
|
||||
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,
|
||||
)
|
@@ -27,13 +27,6 @@ def test_limited(reset: None) -> None:
|
||||
)
|
||||
assert response.status_code == 200
|
||||
|
||||
# test basic endpoints
|
||||
response = requests.get(
|
||||
f"{API_SERVER_URL}/input_prompt",
|
||||
headers=api_key.headers,
|
||||
)
|
||||
assert response.status_code == 403
|
||||
|
||||
# test admin endpoints
|
||||
response = requests.get(
|
||||
f"{API_SERVER_URL}/admin/api-key",
|
||||
|
@@ -1,46 +0,0 @@
|
||||
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,
|
||||
};
|
||||
};
|
@@ -1,31 +0,0 @@
|
||||
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;
|
||||
}
|
@@ -1,69 +0,0 @@
|
||||
import React from "react";
|
||||
import { Formik, Form } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { Button } from "@/components/ui/button";
|
||||
|
||||
import { BookstackIcon } from "@/components/icons/icons";
|
||||
import { AddPromptModalProps } from "../interfaces";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { Modal } from "@/components/Modal";
|
||||
|
||||
const AddPromptSchema = Yup.object().shape({
|
||||
title: Yup.string().required("Title is required"),
|
||||
prompt: Yup.string().required("Prompt is required"),
|
||||
});
|
||||
|
||||
const AddPromptModal = ({ onClose, onSubmit }: AddPromptModalProps) => {
|
||||
return (
|
||||
<Modal onOutsideClick={onClose} width="w-full max-w-3xl">
|
||||
<Formik
|
||||
initialValues={{
|
||||
title: "",
|
||||
prompt: "",
|
||||
}}
|
||||
validationSchema={AddPromptSchema}
|
||||
onSubmit={(values, { setSubmitting }) => {
|
||||
onSubmit({
|
||||
prompt: values.title,
|
||||
content: values.prompt,
|
||||
});
|
||||
setSubmitting(false);
|
||||
onClose();
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, setFieldValue }) => (
|
||||
<Form>
|
||||
<h2 className="w-full text-2xl gap-x-2 text-emphasis font-bold mb-3 flex items-center">
|
||||
<BookstackIcon size={20} />
|
||||
Add prompt
|
||||
</h2>
|
||||
|
||||
<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')"
|
||||
/>
|
||||
|
||||
<Button
|
||||
type="submit"
|
||||
className="w-full"
|
||||
disabled={isSubmitting}
|
||||
variant="submit"
|
||||
>
|
||||
Add prompt
|
||||
</Button>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AddPromptModal;
|
@@ -1,138 +0,0 @@
|
||||
import React from "react";
|
||||
import { Formik, Form, Field, ErrorMessage } from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import { Button } from "@/components/ui/button";
|
||||
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 (
|
||||
<Modal onOutsideClick={onClose} width="max-w-xl">
|
||||
<p>Failed to load prompt data</p>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
if (!promptData)
|
||||
return (
|
||||
<Modal onOutsideClick={onClose} width="w-full max-w-xl">
|
||||
<p>Loading...</p>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
return (
|
||||
<Modal onOutsideClick={onClose} width="w-full 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 className="items-stretch">
|
||||
<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={Textarea}
|
||||
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"
|
||||
disabled={
|
||||
isSubmitting ||
|
||||
(values.prompt === promptData.prompt &&
|
||||
values.content === promptData.content &&
|
||||
values.active === promptData.active)
|
||||
}
|
||||
>
|
||||
{isSubmitting ? "Updating..." : "Update prompt"}
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default EditPromptModal;
|
@@ -1,32 +0,0 @@
|
||||
"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;
|
@@ -1,249 +0,0 @@
|
||||
"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,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@/components/ui/table";
|
||||
import { FilterDropdown } from "@/components/search/filtering/FilterDropdown";
|
||||
import { FiTag } from "react-icons/fi";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { InputPrompt } from "./interfaces";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { TableHeader } from "@/components/ui/table";
|
||||
|
||||
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();
|
||||
setConfirmDeletionId(null);
|
||||
};
|
||||
|
||||
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 && (
|
||||
<DeleteEntityModal
|
||||
onClose={() => setConfirmDeletionId(null)}
|
||||
onSubmit={() => handleDelete(confirmDeletionId)}
|
||||
entityType="prompt"
|
||||
entityName={
|
||||
paginatedPromptLibrary.find(
|
||||
(prompt) => prompt.id === confirmDeletionId
|
||||
)?.prompt ?? ""
|
||||
}
|
||||
/>
|
||||
)}
|
||||
|
||||
<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-col items-stretch w-full flex-wrap pb-4 mt-3">
|
||||
{selectedStatus.map((status) => (
|
||||
<CategoryBubble
|
||||
key={status}
|
||||
name={status}
|
||||
onDelete={() => handleStatusSelect(status)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className="mx-auto overflow-x-auto">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
{columns.map((column) => (
|
||||
<TableHead key={column.key}>{column.name}</TableHead>
|
||||
))}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paginatedPromptLibrary.length > 0 ? (
|
||||
paginatedPromptLibrary
|
||||
.filter((prompt) => !(!isPublic && prompt.is_public))
|
||||
.map((item) => (
|
||||
<TableRow key={item.id}>
|
||||
<TableCell>{item.prompt}</TableCell>
|
||||
<TableCell
|
||||
className="
|
||||
max-w-xs
|
||||
overflow-hidden
|
||||
text-ellipsis
|
||||
break-words
|
||||
"
|
||||
>
|
||||
{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>
|
||||
);
|
||||
};
|
@@ -1,150 +0,0 @@
|
||||
"use client";
|
||||
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import Text from "@/components/ui/text";
|
||||
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" : ""}
|
||||
variant="navigate"
|
||||
size="sm"
|
||||
>
|
||||
New Prompt
|
||||
</Button>
|
||||
|
||||
<Separator />
|
||||
|
||||
<div>
|
||||
<PromptLibraryTable
|
||||
isPublic={isPublic}
|
||||
promptLibrary={promptLibrary}
|
||||
setPopup={setPopup}
|
||||
refresh={refreshPrompts}
|
||||
handleEdit={handleEdit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
@@ -1,51 +0,0 @@
|
||||
"use client";
|
||||
import SidebarWrapper from "../SidebarWrapper";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { User } from "@/lib/types";
|
||||
|
||||
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,
|
||||
}: {
|
||||
chatSessions: ChatSession[];
|
||||
folders: Folder[];
|
||||
initiallyToggled: boolean;
|
||||
openedFolders?: { [key: number]: boolean };
|
||||
}) {
|
||||
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}
|
||||
>
|
||||
<div className="mx-auto w-searchbar-xs 2xl:w-searchbar-sm 3xl:w-searchbar">
|
||||
<AssistantsPageTitle>Prompt Gallery</AssistantsPageTitle>
|
||||
<PromptSection
|
||||
promptLibrary={promptLibrary || []}
|
||||
isLoading={promptLibraryIsLoading}
|
||||
error={promptLibraryError}
|
||||
refreshPrompts={refreshPrompts}
|
||||
isPublic={false}
|
||||
centering
|
||||
/>
|
||||
</div>
|
||||
</SidebarWrapper>
|
||||
);
|
||||
}
|
@@ -135,7 +135,6 @@ export function ChatPage({
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
userInputPrompts,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
refreshChatSessions,
|
||||
@@ -2744,7 +2743,6 @@ export function ChatPage({
|
||||
chatState={currentSessionChatState}
|
||||
stopGenerating={stopGenerating}
|
||||
openModelSettings={() => setSettingsToggled(true)}
|
||||
inputPrompts={userInputPrompts}
|
||||
showDocs={() => setDocumentSelection(true)}
|
||||
selectedDocuments={selectedDocuments}
|
||||
// assistant stuff
|
||||
|
@@ -2,7 +2,7 @@ import React, { useContext, useEffect, useRef, useState } from "react";
|
||||
import { FiPlusCircle, FiPlus, FiInfo, FiX, FiSearch } 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, LlmOverrideManager } from "@/lib/hooks";
|
||||
import { SelectedFilterDisplay } from "./SelectedFilterDisplay";
|
||||
import { useChatContext } from "@/components/context/ChatContext";
|
||||
@@ -58,7 +58,6 @@ interface ChatInputBarProps {
|
||||
llmOverrideManager: LlmOverrideManager;
|
||||
chatState: ChatState;
|
||||
alternativeAssistant: Persona | null;
|
||||
inputPrompts: InputPrompt[];
|
||||
// assistants
|
||||
selectedAssistant: Persona;
|
||||
setSelectedAssistant: (assistant: Persona) => void;
|
||||
@@ -98,7 +97,6 @@ export function ChatInputBar({
|
||||
textAreaRef,
|
||||
alternativeAssistant,
|
||||
chatSessionId,
|
||||
inputPrompts,
|
||||
toggleFilters,
|
||||
}: ChatInputBarProps) {
|
||||
useEffect(() => {
|
||||
@@ -137,7 +135,6 @@ 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);
|
||||
|
||||
@@ -146,19 +143,6 @@ export function ChatInputBar({
|
||||
setTabbingIconIndex(0);
|
||||
};
|
||||
|
||||
const hidePrompts = () => {
|
||||
setTimeout(() => {
|
||||
setShowPrompts(false);
|
||||
}, 50);
|
||||
|
||||
setTabbingIconIndex(0);
|
||||
};
|
||||
|
||||
const updateInputPrompt = (prompt: InputPrompt) => {
|
||||
hidePrompts();
|
||||
setMessage(`${prompt.content}`);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
@@ -168,7 +152,6 @@ export function ChatInputBar({
|
||||
!interactionsRef.current.contains(event.target as Node))
|
||||
) {
|
||||
hideSuggestions();
|
||||
hidePrompts();
|
||||
}
|
||||
};
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
@@ -198,24 +181,10 @@ export function ChatInputBar({
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
handleAssistantInput(text);
|
||||
handlePromptInput(text);
|
||||
};
|
||||
|
||||
const assistantTagOptions = assistantOptions.filter((assistant) =>
|
||||
@@ -227,40 +196,18 @@ export function ChatInputBar({
|
||||
)
|
||||
);
|
||||
|
||||
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) || showPrompts) &&
|
||||
showSuggestions &&
|
||||
assistantTagOptions.length > 0 &&
|
||||
(e.key === "Tab" || e.key == "Enter")
|
||||
) {
|
||||
e.preventDefault();
|
||||
|
||||
if (
|
||||
(tabbingIconIndex == assistantTagOptions.length && showSuggestions) ||
|
||||
(tabbingIconIndex == filteredPrompts.length && showPrompts)
|
||||
) {
|
||||
if (showPrompts) {
|
||||
window.open("/prompts", "_self");
|
||||
} else {
|
||||
if (tabbingIconIndex == assistantTagOptions.length && showSuggestions) {
|
||||
window.open("/assistants/new", "_self");
|
||||
}
|
||||
} else {
|
||||
if (showPrompts) {
|
||||
const uppity =
|
||||
filteredPrompts[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
|
||||
updateInputPrompt(uppity);
|
||||
} else {
|
||||
const option =
|
||||
assistantTagOptions[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
|
||||
@@ -268,8 +215,7 @@ export function ChatInputBar({
|
||||
updatedTaggedAssistant(option);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!showPrompts && !showSuggestions) {
|
||||
if (!showSuggestions) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -277,10 +223,7 @@ export function ChatInputBar({
|
||||
e.preventDefault();
|
||||
|
||||
setTabbingIconIndex((tabbingIconIndex) =>
|
||||
Math.min(
|
||||
tabbingIconIndex + 1,
|
||||
showPrompts ? filteredPrompts.length : assistantTagOptions.length
|
||||
)
|
||||
Math.min(tabbingIconIndex + 1, assistantTagOptions.length)
|
||||
);
|
||||
} else if (e.key === "ArrowUp") {
|
||||
e.preventDefault();
|
||||
@@ -341,48 +284,6 @@ export function ChatInputBar({
|
||||
</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: InputPrompt, index: number) => (
|
||||
<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="text-left flex-grow mr-auto line-clamp-1">
|
||||
{currentPrompt.id == selectedAssistant.id &&
|
||||
"(default) "}
|
||||
{currentPrompt.content?.trim()}
|
||||
</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> */}
|
||||
@@ -534,7 +435,6 @@ export function ChatInputBar({
|
||||
onKeyDown={(event) => {
|
||||
if (
|
||||
event.key === "Enter" &&
|
||||
!showPrompts &&
|
||||
!showSuggestions &&
|
||||
!event.shiftKey &&
|
||||
!(event.nativeEvent as any).isComposing
|
||||
|
@@ -31,7 +31,6 @@ export default async function Page(props: {
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
userInputPrompts,
|
||||
ccPairs,
|
||||
} = data;
|
||||
|
||||
@@ -53,7 +52,6 @@ export default async function Page(props: {
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
userInputPrompts,
|
||||
shouldShowWelcomeModal,
|
||||
defaultAssistantId,
|
||||
}}
|
||||
|
@@ -163,15 +163,6 @@ 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-history-sidebar-button-hover cursor-pointer transition-all duration-150 flex gap-x-2"
|
||||
>
|
||||
<ClosedBookIcon className="h-4 w-4 my-auto text-text-history-sidebar-button" />
|
||||
<p className="my-auto flex items-center text-sm ">
|
||||
Manage Prompts
|
||||
</p>
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
<div className="border-b border-divider-history-sidebar-bar pb-4 mx-3" />
|
||||
|
@@ -1,28 +0,0 @@
|
||||
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(props: {
|
||||
searchParams: Promise<{ [key: string]: string }>;
|
||||
}) {
|
||||
const searchParams = await props.searchParams;
|
||||
noStore();
|
||||
|
||||
const data = await fetchChatData(searchParams);
|
||||
|
||||
if ("redirect" in data) {
|
||||
redirect(data.redirect);
|
||||
}
|
||||
|
||||
const { chatSessions, folders, openedFolders, toggleSidebar } = data;
|
||||
|
||||
return (
|
||||
<WrappedPrompts
|
||||
initiallyToggled={toggleSidebar}
|
||||
chatSessions={chatSessions}
|
||||
folders={folders}
|
||||
openedFolders={openedFolders}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -169,18 +169,6 @@ export function ClientLayout({
|
||||
),
|
||||
link: "/admin/tools",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
<div className="flex">
|
||||
<ClosedBookIcon
|
||||
className="text-icon-settings-sidebar"
|
||||
size={18}
|
||||
/>
|
||||
<div className="ml-1">Prompt Library</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/prompt-library",
|
||||
},
|
||||
]
|
||||
: []),
|
||||
...(enableEnterprise
|
||||
|
@@ -9,11 +9,8 @@ import {
|
||||
ValidSources,
|
||||
} from "@/lib/types";
|
||||
import { ChatSession } from "@/app/chat/interfaces";
|
||||
import { Persona } from "@/app/admin/assistants/interfaces";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
import { InputPrompt } from "@/app/admin/prompt-library/interfaces";
|
||||
import { personaComparator } from "@/app/admin/assistants/lib";
|
||||
|
||||
interface ChatContextProps {
|
||||
chatSessions: ChatSession[];
|
||||
@@ -26,7 +23,6 @@ interface ChatContextProps {
|
||||
llmProviders: LLMProviderDescriptor[];
|
||||
folders: Folder[];
|
||||
openedFolders: Record<string, boolean>;
|
||||
userInputPrompts: InputPrompt[];
|
||||
shouldShowWelcomeModal?: boolean;
|
||||
shouldDisplaySourcesIncompleteModal?: boolean;
|
||||
defaultAssistantId?: number;
|
||||
|
@@ -13,7 +13,6 @@ 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 "@/components/embedding/interfaces";
|
||||
import { Settings } from "@/app/admin/settings/interfaces";
|
||||
import { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
@@ -42,7 +41,6 @@ interface FetchChatDataResult {
|
||||
toggleSidebar: boolean;
|
||||
finalDocumentSidebarInitialWidth?: number;
|
||||
shouldShowWelcomeModal: boolean;
|
||||
userInputPrompts: InputPrompt[];
|
||||
}
|
||||
|
||||
export async function fetchChatData(searchParams: {
|
||||
@@ -58,7 +56,6 @@ export async function fetchChatData(searchParams: {
|
||||
fetchSS("/query/valid-tags"),
|
||||
fetchLLMProvidersSS(),
|
||||
fetchSS("/folder"),
|
||||
fetchSS("/input_prompt?include_public=true"),
|
||||
];
|
||||
|
||||
let results: (
|
||||
@@ -87,7 +84,6 @@ export async function fetchChatData(searchParams: {
|
||||
const tagsResponse = results[5] as Response | null;
|
||||
const llmProviders = (results[6] || []) as LLMProviderDescriptor[];
|
||||
const foldersResponse = results[7] as Response | null;
|
||||
const userInputPromptsResponse = results[8] as Response | null;
|
||||
|
||||
const authDisabled = authTypeMetadata?.authType === "disabled";
|
||||
if (!authDisabled && !user) {
|
||||
@@ -142,15 +138,6 @@ 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 tags: Tag[] = [];
|
||||
if (tagsResponse?.ok) {
|
||||
tags = (await tagsResponse.json()).tags;
|
||||
@@ -212,6 +199,5 @@ export async function fetchChatData(searchParams: {
|
||||
finalDocumentSidebarInitialWidth,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
userInputPrompts,
|
||||
};
|
||||
}
|
||||
|
@@ -13,7 +13,6 @@ 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 { fetchLLMProvidersSS } from "@/lib/llm/fetchLLMs";
|
||||
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
|
||||
import { Folder } from "@/app/chat/folders/interfaces";
|
||||
@@ -44,7 +43,6 @@ interface FetchChatDataResult {
|
||||
finalDocumentSidebarInitialWidth?: number;
|
||||
shouldShowWelcomeModal?: boolean;
|
||||
shouldDisplaySourcesIncompleteModal?: boolean;
|
||||
userInputPrompts?: InputPrompt[];
|
||||
}
|
||||
|
||||
type FetchOption =
|
||||
@@ -55,8 +53,7 @@ type FetchOption =
|
||||
| "assistants"
|
||||
| "tags"
|
||||
| "llmProviders"
|
||||
| "folders"
|
||||
| "userInputPrompts";
|
||||
| "folders";
|
||||
|
||||
/*
|
||||
NOTE: currently unused, but leaving here for future use.
|
||||
@@ -76,7 +73,6 @@ export async function fetchSomeChatData(
|
||||
tags: () => fetchSS("/query/valid-tags"),
|
||||
llmProviders: fetchLLMProvidersSS,
|
||||
folders: () => fetchSS("/folder"),
|
||||
userInputPrompts: () => fetchSS("/input_prompt?include_public=true"),
|
||||
};
|
||||
|
||||
// Always fetch auth type metadata
|
||||
@@ -152,11 +148,6 @@ export async function fetchSomeChatData(
|
||||
? ((await result.json()) as { folders: Folder[] }).folders
|
||||
: [];
|
||||
break;
|
||||
case "userInputPrompts":
|
||||
result.userInputPrompts = result?.ok
|
||||
? ((await result.json()) as InputPrompt[])
|
||||
: [];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -1,16 +0,0 @@
|
||||
import { test, expect } from "@chromatic-com/playwright";
|
||||
|
||||
test(
|
||||
"Admin - Custom Assistants - Prompt Library",
|
||||
{
|
||||
tag: "@admin",
|
||||
},
|
||||
async ({ page }, testInfo) => {
|
||||
// Test simple loading
|
||||
await page.goto("http://localhost:3000/admin/prompt-library");
|
||||
await expect(page.locator("h1.text-3xl")).toHaveText("Prompt Library");
|
||||
await expect(page.locator("p.text-sm")).toHaveText(
|
||||
/^Create prompts that can be accessed/
|
||||
);
|
||||
}
|
||||
);
|
Reference in New Issue
Block a user