Add Prompt library (#1990)

This commit is contained in:
pablodanswer 2024-08-01 08:40:35 -07:00 committed by GitHub
parent 445f7e70ba
commit 459bd46846
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 1619 additions and 48 deletions

View 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")

View File

@ -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

View 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

View File

@ -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)

View File

@ -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

View 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())

View File

@ -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

View File

@ -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)

View 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]

View 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,
)

View File

@ -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,

View 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,
};
};

View 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;
}

View 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;

View 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;

View 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;

View 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"
>
&times;
</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>
);
};

View 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>
);
};

View File

@ -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";

View File

@ -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>

View File

@ -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

View 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>
)}
/>
);
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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>

View File

@ -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

View File

@ -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" />

View 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}
/>
);
}

View File

@ -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",
},
],
},
{

View File

@ -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,

View File

@ -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);

View File

@ -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>
);
};

View File

@ -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
) : (

View File

@ -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,
};
}