mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-27 12:29:41 +02:00
Allow persona usage for Slack bots
This commit is contained in:
@@ -89,6 +89,7 @@ def handle_message(
|
||||
sender_id = message_info.sender
|
||||
bipass_filters = message_info.bipass_filters
|
||||
is_bot_msg = message_info.is_bot_msg
|
||||
persona = channel_config.persona if channel_config else None
|
||||
|
||||
logger = cast(
|
||||
logging.Logger,
|
||||
@@ -96,9 +97,9 @@ def handle_message(
|
||||
)
|
||||
|
||||
document_set_names: list[str] | None = None
|
||||
if channel_config and channel_config.persona:
|
||||
if persona:
|
||||
document_set_names = [
|
||||
document_set.name for document_set in channel_config.persona.document_sets
|
||||
document_set.name for document_set in persona.document_sets
|
||||
]
|
||||
|
||||
# List of user id to send message to, if None, send to everyone in channel
|
||||
@@ -194,7 +195,10 @@ def handle_message(
|
||||
# for an existing chat session associated with this thread
|
||||
with Session(get_sqlalchemy_engine()) as db_session:
|
||||
chat_session = create_chat_session(
|
||||
db_session=db_session, description="", user_id=None
|
||||
db_session=db_session,
|
||||
description="",
|
||||
user_id=None,
|
||||
persona_id=persona.id if persona else None,
|
||||
)
|
||||
chat_session_id = chat_session.id
|
||||
|
||||
|
@@ -325,6 +325,7 @@ def upsert_persona(
|
||||
default_persona: bool = False,
|
||||
document_sets: list[DocumentSetDBModel] | None = None,
|
||||
commit: bool = True,
|
||||
overwrite_duplicate_named_persona: bool = False,
|
||||
) -> Persona:
|
||||
persona = db_session.query(Persona).filter_by(id=persona_id).first()
|
||||
if persona and persona.deleted:
|
||||
@@ -336,9 +337,13 @@ def upsert_persona(
|
||||
persona = fetch_default_persona_by_name(name, db_session)
|
||||
else:
|
||||
# only one persona with the same name should exist
|
||||
if fetch_persona_by_name(name, db_session):
|
||||
persona_with_same_name = fetch_persona_by_name(name, db_session)
|
||||
if persona_with_same_name and not overwrite_duplicate_named_persona:
|
||||
raise ValueError("Trying to create a persona with a duplicate name")
|
||||
|
||||
# set "existing" persona to the one with the same name so we can override it
|
||||
persona = persona_with_same_name
|
||||
|
||||
if persona:
|
||||
persona.name = name
|
||||
persona.description = description
|
||||
|
@@ -27,7 +27,7 @@ def _cleanup_relationships(db_session: Session, persona_id: int) -> None:
|
||||
db_session.delete(rel)
|
||||
|
||||
|
||||
def _create_slack_bot_persona(
|
||||
def create_slack_bot_persona(
|
||||
db_session: Session,
|
||||
channel_names: list[str],
|
||||
document_sets: list[int],
|
||||
@@ -47,6 +47,7 @@ def _create_slack_bot_persona(
|
||||
default_persona=False,
|
||||
db_session=db_session,
|
||||
commit=False,
|
||||
overwrite_duplicate_named_persona=True,
|
||||
)
|
||||
|
||||
if existing_persona_id:
|
||||
@@ -62,20 +63,12 @@ def _create_slack_bot_persona(
|
||||
|
||||
|
||||
def insert_slack_bot_config(
|
||||
document_sets: list[int],
|
||||
persona_id: int | None,
|
||||
channel_config: ChannelConfig,
|
||||
db_session: Session,
|
||||
) -> SlackBotConfig:
|
||||
persona = None
|
||||
if document_sets:
|
||||
persona = _create_slack_bot_persona(
|
||||
db_session=db_session,
|
||||
channel_names=channel_config["channel_names"],
|
||||
document_sets=document_sets,
|
||||
)
|
||||
|
||||
slack_bot_config = SlackBotConfig(
|
||||
persona_id=persona.id if persona else None,
|
||||
persona_id=persona_id,
|
||||
channel_config=channel_config,
|
||||
)
|
||||
db_session.add(slack_bot_config)
|
||||
@@ -86,7 +79,7 @@ def insert_slack_bot_config(
|
||||
|
||||
def update_slack_bot_config(
|
||||
slack_bot_config_id: int,
|
||||
document_sets: list[int],
|
||||
persona_id: int | None,
|
||||
channel_config: ChannelConfig,
|
||||
db_session: Session,
|
||||
) -> SlackBotConfig:
|
||||
@@ -97,31 +90,29 @@ def update_slack_bot_config(
|
||||
raise ValueError(
|
||||
f"Unable to find slack bot config with ID {slack_bot_config_id}"
|
||||
)
|
||||
|
||||
# get the existing persona id before updating the object
|
||||
existing_persona_id = slack_bot_config.persona_id
|
||||
|
||||
persona = None
|
||||
if document_sets:
|
||||
persona = _create_slack_bot_persona(
|
||||
db_session=db_session,
|
||||
channel_names=channel_config["channel_names"],
|
||||
document_sets=document_sets,
|
||||
existing_persona_id=slack_bot_config.persona_id,
|
||||
# update the config
|
||||
# NOTE: need to do this before cleaning up the old persona or else we
|
||||
# will encounter `violates foreign key constraint` errors
|
||||
slack_bot_config.persona_id = persona_id
|
||||
slack_bot_config.channel_config = channel_config
|
||||
|
||||
# if the persona has changed, then clean up the old persona
|
||||
if persona_id != existing_persona_id and existing_persona_id:
|
||||
existing_persona = db_session.scalar(
|
||||
select(Persona).where(Persona.id == existing_persona_id)
|
||||
)
|
||||
else:
|
||||
# if no document sets and an existing persona exists, then
|
||||
# remove persona + persona -> document set relationships
|
||||
if existing_persona_id:
|
||||
# if the existing persona was one created just for use with this Slack Bot,
|
||||
# then clean it up
|
||||
if existing_persona and existing_persona.name.startswith(
|
||||
SLACK_BOT_PERSONA_PREFIX
|
||||
):
|
||||
_cleanup_relationships(
|
||||
db_session=db_session, persona_id=existing_persona_id
|
||||
)
|
||||
existing_persona = db_session.scalar(
|
||||
select(Persona).where(Persona.id == existing_persona_id)
|
||||
)
|
||||
db_session.delete(existing_persona)
|
||||
|
||||
slack_bot_config.persona_id = persona.id if persona else None
|
||||
slack_bot_config.channel_config = channel_config
|
||||
db_session.commit()
|
||||
|
||||
return slack_bot_config
|
||||
@@ -141,11 +132,30 @@ def remove_slack_bot_config(
|
||||
|
||||
existing_persona_id = slack_bot_config.persona_id
|
||||
if existing_persona_id:
|
||||
_cleanup_relationships(db_session=db_session, persona_id=existing_persona_id)
|
||||
existing_persona = db_session.scalar(
|
||||
select(Persona).where(Persona.id == existing_persona_id)
|
||||
)
|
||||
# if the existing persona was one created just for use with this Slack Bot,
|
||||
# then clean it up
|
||||
if existing_persona and existing_persona.name.startswith(
|
||||
SLACK_BOT_PERSONA_PREFIX
|
||||
):
|
||||
_cleanup_relationships(
|
||||
db_session=db_session, persona_id=existing_persona_id
|
||||
)
|
||||
db_session.delete(existing_persona)
|
||||
|
||||
db_session.delete(slack_bot_config)
|
||||
db_session.commit()
|
||||
|
||||
|
||||
def fetch_slack_bot_config(
|
||||
db_session: Session, slack_bot_config_id: int
|
||||
) -> SlackBotConfig | None:
|
||||
return db_session.scalar(
|
||||
select(SlackBotConfig).where(SlackBotConfig.id == slack_bot_config_id)
|
||||
)
|
||||
|
||||
|
||||
def fetch_slack_bot_configs(db_session: Session) -> Sequence[SlackBotConfig]:
|
||||
return db_session.scalars(select(SlackBotConfig)).all()
|
||||
|
@@ -15,10 +15,12 @@ from danswer.db.chat import fetch_chat_session_by_id
|
||||
from danswer.db.feedback import create_query_event
|
||||
from danswer.db.feedback import update_query_event_llm_answer
|
||||
from danswer.db.feedback import update_query_event_retrieved_documents
|
||||
from danswer.db.models import Persona
|
||||
from danswer.db.models import User
|
||||
from danswer.direct_qa.factory import get_default_qa_model
|
||||
from danswer.direct_qa.factory import get_qa_model_for_persona
|
||||
from danswer.direct_qa.interfaces import DanswerAnswerPiece
|
||||
from danswer.direct_qa.interfaces import QAModel
|
||||
from danswer.direct_qa.interfaces import StreamingError
|
||||
from danswer.direct_qa.models import LLMMetricsContainer
|
||||
from danswer.direct_qa.qa_utils import get_chunks_for_qa
|
||||
@@ -45,6 +47,13 @@ from danswer.utils.timing import log_generator_function_time
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def _get_qa_model(persona: Persona | None) -> QAModel:
|
||||
if persona and (persona.hint_text or persona.system_text):
|
||||
return get_qa_model_for_persona(persona=persona)
|
||||
|
||||
return get_default_qa_model()
|
||||
|
||||
|
||||
@log_function_time()
|
||||
def answer_qa_query(
|
||||
new_message_request: NewMessageRequest,
|
||||
@@ -74,12 +83,30 @@ def answer_qa_query(
|
||||
user_id=user.id if user is not None else None,
|
||||
db_session=db_session,
|
||||
)
|
||||
chat_session = fetch_chat_session_by_id(
|
||||
chat_session_id=new_message_request.chat_session_id, db_session=db_session
|
||||
)
|
||||
persona = chat_session.persona
|
||||
persona_skip_llm_chunk_filter = (
|
||||
not persona.apply_llm_relevance_filter if persona else None
|
||||
)
|
||||
persona_num_chunks = persona.num_chunks if persona else None
|
||||
if persona:
|
||||
logger.info(f"Using persona: {persona.name}")
|
||||
logger.info(
|
||||
"Persona retrieval settings: skip_llm_chunk_filter: "
|
||||
f"{persona_skip_llm_chunk_filter}, "
|
||||
f"num_chunks: {persona_num_chunks}"
|
||||
)
|
||||
|
||||
retrieval_request, predicted_search_type, predicted_flow = retrieval_preprocessing(
|
||||
new_message_request=new_message_request,
|
||||
user=user,
|
||||
db_session=db_session,
|
||||
bypass_acl=bypass_acl,
|
||||
skip_llm_chunk_filter=persona_skip_llm_chunk_filter
|
||||
if persona_skip_llm_chunk_filter is not None
|
||||
else DISABLE_LLM_CHUNK_FILTER,
|
||||
)
|
||||
|
||||
# Set flow as search so frontend doesn't ask the user if they want to run QA over more docs
|
||||
@@ -123,10 +150,7 @@ def answer_qa_query(
|
||||
)
|
||||
|
||||
try:
|
||||
qa_model = get_default_qa_model(
|
||||
timeout=answer_generation_timeout,
|
||||
real_time_flow=new_message_request.real_time,
|
||||
)
|
||||
qa_model = _get_qa_model(persona)
|
||||
except Exception as e:
|
||||
return partial_response(
|
||||
answer=None,
|
||||
@@ -294,10 +318,7 @@ def answer_qa_query_stream(
|
||||
return
|
||||
|
||||
try:
|
||||
if not persona:
|
||||
qa_model = get_default_qa_model()
|
||||
else:
|
||||
qa_model = get_qa_model_for_persona(persona=persona)
|
||||
qa_model = _get_qa_model(persona)
|
||||
except Exception as e:
|
||||
logger.exception("Unable to get QA model")
|
||||
error = StreamingError(error=str(e))
|
||||
|
@@ -1,4 +1,7 @@
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel
|
||||
from pydantic import root_validator
|
||||
from pydantic import validator
|
||||
|
||||
from danswer.auth.schemas import UserRole
|
||||
@@ -6,7 +9,7 @@ from danswer.configs.constants import AuthType
|
||||
from danswer.danswerbot.slack.config import VALID_SLACK_FILTERS
|
||||
from danswer.db.models import AllowedAnswerFilters
|
||||
from danswer.db.models import ChannelConfig
|
||||
from danswer.server.features.document_set.models import DocumentSet
|
||||
from danswer.server.features.persona.models import PersonaSnapshot
|
||||
|
||||
|
||||
class VersionResponse(BaseModel):
|
||||
@@ -62,7 +65,8 @@ class SlackBotConfigCreationRequest(BaseModel):
|
||||
# in the future, `document_sets` will probably be replaced
|
||||
# by an optional `PersonaSnapshot` object. Keeping it like this
|
||||
# for now for simplicity / speed of development
|
||||
document_sets: list[int]
|
||||
document_sets: list[int] | None
|
||||
persona_id: int | None # NOTE: only one of `document_sets` / `persona_id` should be set
|
||||
channel_names: list[str]
|
||||
respond_tag_only: bool = False
|
||||
# If no team members, assume respond in the channel to everyone
|
||||
@@ -77,12 +81,17 @@ class SlackBotConfigCreationRequest(BaseModel):
|
||||
)
|
||||
return value
|
||||
|
||||
@root_validator
|
||||
def validate_document_sets_and_persona_id(
|
||||
cls, values: dict[str, Any]
|
||||
) -> dict[str, Any]:
|
||||
if values.get("document_sets") and values.get("persona_id"):
|
||||
raise ValueError("Only one of `document_sets` / `persona_id` should be set")
|
||||
|
||||
return values
|
||||
|
||||
|
||||
class SlackBotConfig(BaseModel):
|
||||
id: int
|
||||
# currently, a persona is created for each slack bot config
|
||||
# in the future, `document_sets` will probably be replaced
|
||||
# by an optional `PersonaSnapshot` object. Keeping it like this
|
||||
# for now for simplicity / speed of development
|
||||
document_sets: list[DocumentSet]
|
||||
persona: PersonaSnapshot | None
|
||||
channel_config: ChannelConfig
|
||||
|
@@ -10,12 +10,14 @@ from danswer.danswerbot.slack.tokens import save_tokens
|
||||
from danswer.db.engine import get_session
|
||||
from danswer.db.models import ChannelConfig
|
||||
from danswer.db.models import User
|
||||
from danswer.db.slack_bot_config import create_slack_bot_persona
|
||||
from danswer.db.slack_bot_config import fetch_slack_bot_config
|
||||
from danswer.db.slack_bot_config import fetch_slack_bot_configs
|
||||
from danswer.db.slack_bot_config import insert_slack_bot_config
|
||||
from danswer.db.slack_bot_config import remove_slack_bot_config
|
||||
from danswer.db.slack_bot_config import update_slack_bot_config
|
||||
from danswer.dynamic_configs.interface import ConfigNotFoundError
|
||||
from danswer.server.features.document_set.models import DocumentSet
|
||||
from danswer.server.features.persona.models import PersonaSnapshot
|
||||
from danswer.server.manage.models import SlackBotConfig
|
||||
from danswer.server.manage.models import SlackBotConfigCreationRequest
|
||||
from danswer.server.manage.models import SlackBotTokens
|
||||
@@ -83,19 +85,29 @@ def create_slack_bot_config(
|
||||
slack_bot_config_creation_request, None, db_session
|
||||
)
|
||||
|
||||
persona_id = None
|
||||
if slack_bot_config_creation_request.persona_id is not None:
|
||||
persona_id = slack_bot_config_creation_request.persona_id
|
||||
elif slack_bot_config_creation_request.document_sets:
|
||||
persona_id = create_slack_bot_persona(
|
||||
db_session=db_session,
|
||||
channel_names=channel_config["channel_names"],
|
||||
document_sets=slack_bot_config_creation_request.document_sets,
|
||||
existing_persona_id=None,
|
||||
).id
|
||||
|
||||
slack_bot_config_model = insert_slack_bot_config(
|
||||
document_sets=slack_bot_config_creation_request.document_sets,
|
||||
persona_id=persona_id,
|
||||
channel_config=channel_config,
|
||||
db_session=db_session,
|
||||
)
|
||||
return SlackBotConfig(
|
||||
id=slack_bot_config_model.id,
|
||||
document_sets=[
|
||||
DocumentSet.from_model(document_set)
|
||||
for document_set in slack_bot_config_model.persona.document_sets
|
||||
]
|
||||
if slack_bot_config_model.persona
|
||||
else [],
|
||||
persona=(
|
||||
PersonaSnapshot.from_model(slack_bot_config_model.persona)
|
||||
if slack_bot_config_model.persona
|
||||
else None
|
||||
),
|
||||
channel_config=slack_bot_config_model.channel_config,
|
||||
)
|
||||
|
||||
@@ -111,20 +123,39 @@ def patch_slack_bot_config(
|
||||
slack_bot_config_creation_request, slack_bot_config_id, db_session
|
||||
)
|
||||
|
||||
persona_id = None
|
||||
if slack_bot_config_creation_request.persona_id is not None:
|
||||
persona_id = slack_bot_config_creation_request.persona_id
|
||||
elif slack_bot_config_creation_request.document_sets:
|
||||
existing_slack_bot_config = fetch_slack_bot_config(
|
||||
db_session=db_session, slack_bot_config_id=slack_bot_config_id
|
||||
)
|
||||
if existing_slack_bot_config is None:
|
||||
raise HTTPException(
|
||||
status_code=404,
|
||||
detail="Slack bot config not found",
|
||||
)
|
||||
|
||||
persona_id = create_slack_bot_persona(
|
||||
db_session=db_session,
|
||||
channel_names=channel_config["channel_names"],
|
||||
document_sets=slack_bot_config_creation_request.document_sets,
|
||||
existing_persona_id=existing_slack_bot_config.persona_id,
|
||||
).id
|
||||
|
||||
slack_bot_config_model = update_slack_bot_config(
|
||||
slack_bot_config_id=slack_bot_config_id,
|
||||
document_sets=slack_bot_config_creation_request.document_sets,
|
||||
persona_id=persona_id,
|
||||
channel_config=channel_config,
|
||||
db_session=db_session,
|
||||
)
|
||||
return SlackBotConfig(
|
||||
id=slack_bot_config_model.id,
|
||||
document_sets=[
|
||||
DocumentSet.from_model(document_set)
|
||||
for document_set in slack_bot_config_model.persona.document_sets
|
||||
]
|
||||
if slack_bot_config_model.persona
|
||||
else [],
|
||||
persona=(
|
||||
PersonaSnapshot.from_model(slack_bot_config_model.persona)
|
||||
if slack_bot_config_model.persona
|
||||
else None
|
||||
),
|
||||
channel_config=slack_bot_config_model.channel_config,
|
||||
)
|
||||
|
||||
@@ -149,12 +180,11 @@ def list_slack_bot_configs(
|
||||
return [
|
||||
SlackBotConfig(
|
||||
id=slack_bot_config_model.id,
|
||||
document_sets=[
|
||||
DocumentSet.from_model(document_set)
|
||||
for document_set in slack_bot_config_model.persona.document_sets
|
||||
]
|
||||
if slack_bot_config_model.persona
|
||||
else [],
|
||||
persona=(
|
||||
PersonaSnapshot.from_model(slack_bot_config_model.persona)
|
||||
if slack_bot_config_model.persona
|
||||
else None
|
||||
),
|
||||
channel_config=slack_bot_config_model.channel_config,
|
||||
)
|
||||
for slack_bot_config_model in slack_bot_config_models
|
||||
|
@@ -6,27 +6,52 @@ import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||
import {
|
||||
BooleanFormField,
|
||||
Label,
|
||||
SectionHeader,
|
||||
SelectorFormField,
|
||||
SubLabel,
|
||||
TextArrayField,
|
||||
} from "@/components/admin/connectors/Field";
|
||||
import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
|
||||
import { Card, Divider } from "@tremor/react";
|
||||
import {
|
||||
createSlackBotConfig,
|
||||
isPersonaASlackBotPersona,
|
||||
updateSlackBotConfig,
|
||||
} from "./lib";
|
||||
import {
|
||||
Card,
|
||||
Divider,
|
||||
Tab,
|
||||
TabGroup,
|
||||
TabList,
|
||||
TabPanel,
|
||||
TabPanels,
|
||||
Text,
|
||||
} from "@tremor/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
interface SetCreationPopupProps {
|
||||
documentSets: DocumentSet[];
|
||||
existingSlackBotConfig?: SlackBotConfig;
|
||||
}
|
||||
import { Persona } from "../personas/interfaces";
|
||||
import { useState } from "react";
|
||||
import { BookmarkIcon, RobotIcon } from "@/components/icons/icons";
|
||||
|
||||
export const SlackBotCreationForm = ({
|
||||
documentSets,
|
||||
personas,
|
||||
existingSlackBotConfig,
|
||||
}: SetCreationPopupProps) => {
|
||||
}: {
|
||||
documentSets: DocumentSet[];
|
||||
personas: Persona[];
|
||||
existingSlackBotConfig?: SlackBotConfig;
|
||||
}) => {
|
||||
const isUpdate = existingSlackBotConfig !== undefined;
|
||||
console.log(existingSlackBotConfig);
|
||||
const { popup, setPopup } = usePopup();
|
||||
const router = useRouter();
|
||||
|
||||
const existingSlackBotUsesPersona = existingSlackBotConfig?.persona
|
||||
? !isPersonaASlackBotPersona(existingSlackBotConfig.persona)
|
||||
: false;
|
||||
const [usingPersonas, setUsingPersonas] = useState(
|
||||
existingSlackBotUsesPersona
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<Card>
|
||||
@@ -47,11 +72,17 @@ export const SlackBotCreationForm = ({
|
||||
respond_team_member_list:
|
||||
existingSlackBotConfig?.channel_config
|
||||
?.respond_team_member_list || ([] as string[]),
|
||||
document_sets: existingSlackBotConfig
|
||||
? existingSlackBotConfig.document_sets.map(
|
||||
(documentSet) => documentSet.id
|
||||
)
|
||||
: ([] as number[]),
|
||||
document_sets:
|
||||
existingSlackBotConfig && existingSlackBotConfig.persona
|
||||
? existingSlackBotConfig.persona.document_sets.map(
|
||||
(documentSet) => documentSet.id
|
||||
)
|
||||
: ([] as number[]),
|
||||
persona_id:
|
||||
existingSlackBotConfig?.persona &&
|
||||
!isPersonaASlackBotPersona(existingSlackBotConfig.persona)
|
||||
? existingSlackBotConfig.persona.id
|
||||
: null,
|
||||
}}
|
||||
validationSchema={Yup.object().shape({
|
||||
channel_names: Yup.array().of(Yup.string()),
|
||||
@@ -60,6 +91,7 @@ export const SlackBotCreationForm = ({
|
||||
respond_tag_only: Yup.boolean().required(),
|
||||
respond_team_member_list: Yup.array().of(Yup.string()).required(),
|
||||
document_sets: Yup.array().of(Yup.number()),
|
||||
persona_id: Yup.number().nullable(),
|
||||
})}
|
||||
onSubmit={async (values, formikHelpers) => {
|
||||
formikHelpers.setSubmitting(true);
|
||||
@@ -73,6 +105,7 @@ export const SlackBotCreationForm = ({
|
||||
respond_team_member_list: values.respond_team_member_list.filter(
|
||||
(teamMemberEmail) => teamMemberEmail !== ""
|
||||
),
|
||||
usePersona: usingPersonas,
|
||||
};
|
||||
|
||||
let response;
|
||||
@@ -102,6 +135,8 @@ export const SlackBotCreationForm = ({
|
||||
{({ isSubmitting, values }) => (
|
||||
<Form>
|
||||
<div className="px-6 pb-6">
|
||||
<SectionHeader>The Basics</SectionHeader>
|
||||
|
||||
<TextArrayField
|
||||
name="channel_names"
|
||||
label="Channel Names"
|
||||
@@ -120,24 +155,24 @@ export const SlackBotCreationForm = ({
|
||||
}
|
||||
/>
|
||||
<Divider />
|
||||
|
||||
<SectionHeader>When should DanswerBot respond?</SectionHeader>
|
||||
|
||||
<BooleanFormField
|
||||
name="answer_validity_check_enabled"
|
||||
label="Hide Non-Answers"
|
||||
subtext="If set, will only answer questions that the model determines it can answer"
|
||||
/>
|
||||
<Divider />
|
||||
<BooleanFormField
|
||||
name="questionmark_prefilter_enabled"
|
||||
label="Only respond to questions"
|
||||
subtext="If set, will only respond to messages that contain a question mark"
|
||||
/>
|
||||
<Divider />
|
||||
<BooleanFormField
|
||||
name="respond_tag_only"
|
||||
label="Respond to @DanswerBot Only"
|
||||
subtext="If set, DanswerBot will only respond when directly tagged"
|
||||
/>
|
||||
<Divider />
|
||||
<TextArrayField
|
||||
name="respond_team_member_list"
|
||||
label="Team Members Emails"
|
||||
@@ -150,29 +185,67 @@ export const SlackBotCreationForm = ({
|
||||
values={values}
|
||||
/>
|
||||
<Divider />
|
||||
<FieldArray
|
||||
name="document_sets"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div>
|
||||
<div>
|
||||
<Label>Document Sets</Label>
|
||||
<SubLabel>
|
||||
The document sets that DanswerBot should search
|
||||
through. If left blank, DanswerBot will search through
|
||||
all documents.
|
||||
</SubLabel>
|
||||
</div>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap">
|
||||
{documentSets.map((documentSet) => {
|
||||
const ind = values.document_sets.indexOf(
|
||||
documentSet.id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={documentSet.id}
|
||||
className={
|
||||
`
|
||||
|
||||
<div>
|
||||
<SectionHeader>
|
||||
[Optional] Data Sources and Prompts
|
||||
</SectionHeader>
|
||||
<Text>
|
||||
Use either a Persona <b>or</b> Document Sets to control how
|
||||
DanswerBot answers.
|
||||
</Text>
|
||||
<div className="text-dark-tremor-content text-sm">
|
||||
<ul className="list-disc mt-2 ml-4">
|
||||
<li>
|
||||
You should use a Persona if you also want to customize
|
||||
the prompt and retrieval settings.
|
||||
</li>
|
||||
<li>
|
||||
You should use Document Sets if you just want to control
|
||||
which documents DanswerBot uses as references.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<Text className="mt-2">
|
||||
<b>NOTE:</b> whichever tab you are when you submit the form
|
||||
will be the one that is used. For example, if you are on the
|
||||
"Personas" tab, then the Persona will be used, even if you
|
||||
have Document Sets selected.
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<TabGroup
|
||||
index={usingPersonas ? 1 : 0}
|
||||
onIndexChange={(index) => setUsingPersonas(index === 1)}
|
||||
>
|
||||
<TabList className="mt-3 mb-4">
|
||||
<Tab icon={BookmarkIcon}>Document Sets</Tab>
|
||||
<Tab icon={RobotIcon}>Personas</Tab>
|
||||
</TabList>
|
||||
<TabPanels>
|
||||
<TabPanel>
|
||||
<FieldArray
|
||||
name="document_sets"
|
||||
render={(arrayHelpers: ArrayHelpers) => (
|
||||
<div>
|
||||
<div>
|
||||
<SubLabel>
|
||||
The document sets that DanswerBot should search
|
||||
through. If left blank, DanswerBot will search
|
||||
through all documents.
|
||||
</SubLabel>
|
||||
</div>
|
||||
<div className="mb-3 mt-2 flex gap-2 flex-wrap text-sm">
|
||||
{documentSets.map((documentSet) => {
|
||||
const ind = values.document_sets.indexOf(
|
||||
documentSet.id
|
||||
);
|
||||
let isSelected = ind !== -1;
|
||||
return (
|
||||
<div
|
||||
key={documentSet.id}
|
||||
className={
|
||||
`
|
||||
px-3
|
||||
py-1
|
||||
rounded-lg
|
||||
@@ -181,27 +254,50 @@ export const SlackBotCreationForm = ({
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " bg-gray-900 hover:bg-gray-700")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(documentSet.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">{documentSet.name}</div>
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " bg-gray-900 hover:bg-gray-700")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
arrayHelpers.remove(ind);
|
||||
} else {
|
||||
arrayHelpers.push(documentSet.id);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="my-auto">
|
||||
{documentSet.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
</TabPanel>
|
||||
<TabPanel>
|
||||
<SelectorFormField
|
||||
name="persona_id"
|
||||
subtext={`
|
||||
The persona to use when responding to queries. The Default persona acts
|
||||
as a question-answering assistant and has access to all documents indexed by non-private connectors.
|
||||
`}
|
||||
options={personas.map((persona) => {
|
||||
return {
|
||||
name: persona.name,
|
||||
value: persona.id,
|
||||
};
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
includeDefault={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
</TabGroup>
|
||||
|
||||
<Divider />
|
||||
|
||||
<div className="flex">
|
||||
<button
|
||||
type="submit"
|
||||
|
@@ -6,14 +6,17 @@ import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSet, SlackBotConfig } from "@/lib/types";
|
||||
import { Text } from "@tremor/react";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Persona } from "../../personas/interfaces";
|
||||
|
||||
async function Page({ params }: { params: { id: string } }) {
|
||||
const tasks = [
|
||||
fetchSS("/manage/admin/slack-bot/config"),
|
||||
fetchSS("/manage/document-set"),
|
||||
fetchSS("/manage/admin/slack-bot/config", undefined, true),
|
||||
fetchSS("/manage/document-set", undefined, true),
|
||||
fetchSS("/persona", undefined, true),
|
||||
];
|
||||
|
||||
const [slackBotsResponse, documentSetsResponse] = await Promise.all(tasks);
|
||||
const [slackBotsResponse, documentSetsResponse, personasResponse] =
|
||||
await Promise.all(tasks);
|
||||
|
||||
if (!slackBotsResponse.ok) {
|
||||
return (
|
||||
@@ -47,6 +50,16 @@ async function Page({ params }: { params: { id: string } }) {
|
||||
}
|
||||
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
|
||||
|
||||
if (!personasResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch personas - ${await personasResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const personas = (await personasResponse.json()) as Persona[];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto dark">
|
||||
<BackButton />
|
||||
@@ -62,6 +75,7 @@ async function Page({ params }: { params: { id: string } }) {
|
||||
|
||||
<SlackBotCreationForm
|
||||
documentSets={documentSets}
|
||||
personas={personas}
|
||||
existingSlackBotConfig={slackBotConfig}
|
||||
/>
|
||||
</div>
|
||||
|
@@ -1,12 +1,15 @@
|
||||
import { ChannelConfig, SlackBotTokens } from "@/lib/types";
|
||||
import { Persona } from "../personas/interfaces";
|
||||
|
||||
interface SlackBotConfigCreationRequest {
|
||||
document_sets: number[];
|
||||
persona_id: number | null;
|
||||
channel_names: string[];
|
||||
answer_validity_check_enabled: boolean;
|
||||
questionmark_prefilter_enabled: boolean;
|
||||
respond_tag_only: boolean;
|
||||
respond_team_member_list: string[];
|
||||
usePersona: boolean;
|
||||
}
|
||||
|
||||
const buildFiltersFromCreationRequest = (
|
||||
@@ -29,8 +32,10 @@ const buildRequestBodyFromCreationRequest = (
|
||||
channel_names: creationRequest.channel_names,
|
||||
respond_tag_only: creationRequest.respond_tag_only,
|
||||
respond_team_member_list: creationRequest.respond_team_member_list,
|
||||
document_sets: creationRequest.document_sets,
|
||||
answer_filters: buildFiltersFromCreationRequest(creationRequest),
|
||||
...(creationRequest.usePersona
|
||||
? { persona_id: creationRequest.persona_id }
|
||||
: { document_sets: creationRequest.document_sets }),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -77,3 +82,7 @@ export const setSlackBotTokens = async (slackBotTokens: SlackBotTokens) => {
|
||||
body: JSON.stringify(slackBotTokens),
|
||||
});
|
||||
};
|
||||
|
||||
export function isPersonaASlackBotPersona(persona: Persona) {
|
||||
return persona.name.startsWith("__slack_bot_persona__");
|
||||
}
|
||||
|
@@ -6,9 +6,12 @@ import { ErrorCallout } from "@/components/ErrorCallout";
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { BackButton } from "@/components/BackButton";
|
||||
import { Text } from "@tremor/react";
|
||||
import { Persona } from "../../personas/interfaces";
|
||||
|
||||
async function Page() {
|
||||
const documentSetsResponse = await fetchSS("/manage/document-set");
|
||||
const tasks = [fetchSS("/manage/document-set"), fetchSS("/persona")];
|
||||
const [documentSetsResponse, personasResponse] = await Promise.all(tasks);
|
||||
|
||||
if (!documentSetsResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
@@ -19,6 +22,16 @@ async function Page() {
|
||||
}
|
||||
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
|
||||
|
||||
if (!personasResponse.ok) {
|
||||
return (
|
||||
<ErrorCallout
|
||||
errorTitle="Something went wrong :("
|
||||
errorMsg={`Failed to fetch personas - ${await personasResponse.text()}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
const personas = (await personasResponse.json()) as Persona[];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto dark">
|
||||
<BackButton />
|
||||
@@ -32,7 +45,7 @@ async function Page() {
|
||||
DanswerBot behaves in the specified channels.
|
||||
</Text>
|
||||
|
||||
<SlackBotCreationForm documentSets={documentSets} />
|
||||
<SlackBotCreationForm documentSets={documentSets} personas={personas} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -2,17 +2,26 @@
|
||||
|
||||
import { ThreeDotsLoader } from "@/components/Loading";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons";
|
||||
import { SlackBotConfig } from "@/lib/types";
|
||||
import { useState } from "react";
|
||||
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { deleteSlackBotConfig } from "./lib";
|
||||
import { deleteSlackBotConfig, isPersonaASlackBotPersona } from "./lib";
|
||||
import { SlackBotTokensForm } from "./SlackBotTokensForm";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Button, Text, Title } from "@tremor/react";
|
||||
import { FiChevronDown, FiChevronUp } from "react-icons/fi";
|
||||
import {
|
||||
Button,
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeaderCell,
|
||||
TableRow,
|
||||
Text,
|
||||
Title,
|
||||
} from "@tremor/react";
|
||||
import { FiArrowUpRight, FiChevronDown, FiChevronUp } from "react-icons/fi";
|
||||
import Link from "next/link";
|
||||
|
||||
const numToDisplay = 50;
|
||||
@@ -41,119 +50,93 @@ const SlackBotConfigsTable = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BasicTable
|
||||
columns={[
|
||||
{
|
||||
header: "Channels",
|
||||
key: "channels",
|
||||
},
|
||||
{
|
||||
header: "Document Sets",
|
||||
key: "document_sets",
|
||||
},
|
||||
{
|
||||
header: "Team Members",
|
||||
key: "team_members",
|
||||
},
|
||||
{
|
||||
header: "Hide Non-Answers",
|
||||
key: "answer_validity_check_enabled",
|
||||
},
|
||||
{
|
||||
header: "Questions Only",
|
||||
key: "question_mark_only",
|
||||
},
|
||||
{
|
||||
header: "Tags Only",
|
||||
key: "respond_tag_only",
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
key: "delete",
|
||||
width: "50px",
|
||||
},
|
||||
]}
|
||||
data={slackBotConfigs
|
||||
.slice((page - 1) * numToDisplay, page * numToDisplay)
|
||||
.map((slackBotConfig) => {
|
||||
return {
|
||||
channels: (
|
||||
<div className="flex gap-x-2">
|
||||
<Link
|
||||
className="cursor-pointer my-auto"
|
||||
href={`/admin/bot/${slackBotConfig.id}`}
|
||||
>
|
||||
<EditIcon />
|
||||
</Link>
|
||||
<div className="my-auto">
|
||||
{slackBotConfig.channel_config.channel_names
|
||||
.map((channel_name) => `#${channel_name}`)
|
||||
.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
document_sets: (
|
||||
<div>
|
||||
{slackBotConfig.document_sets
|
||||
.map((documentSet) => documentSet.name)
|
||||
.join(", ")}
|
||||
</div>
|
||||
),
|
||||
team_members: (
|
||||
<div>
|
||||
{(
|
||||
slackBotConfig.channel_config.respond_team_member_list || []
|
||||
).join(", ")}
|
||||
</div>
|
||||
),
|
||||
answer_validity_check_enabled: (
|
||||
slackBotConfig.channel_config.answer_filters || []
|
||||
).includes("well_answered_postfilter") ? (
|
||||
<div className="text-gray-300">Yes</div>
|
||||
) : (
|
||||
<div className="text-gray-300">No</div>
|
||||
),
|
||||
question_mark_only: (
|
||||
slackBotConfig.channel_config.answer_filters || []
|
||||
).includes("questionmark_prefilter") ? (
|
||||
<div className="text-gray-300">Yes</div>
|
||||
) : (
|
||||
<div className="text-gray-300">No</div>
|
||||
),
|
||||
respond_tag_only:
|
||||
slackBotConfig.channel_config.respond_tag_only || false ? (
|
||||
<div className="text-gray-300">Yes</div>
|
||||
) : (
|
||||
<div className="text-gray-300">No</div>
|
||||
),
|
||||
delete: (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
const response = await deleteSlackBotConfig(
|
||||
slackBotConfig.id
|
||||
);
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: `Slack bot config "${slackBotConfig.id}" deleted`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
const errorMsg = await response.text();
|
||||
setPopup({
|
||||
message: `Failed to delete Slack bot config - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
<Table>
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Channels</TableHeaderCell>
|
||||
<TableHeaderCell>Persona</TableHeaderCell>
|
||||
<TableHeaderCell>Document Sets</TableHeaderCell>
|
||||
<TableHeaderCell>Delete</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{slackBotConfigs
|
||||
.slice(numToDisplay * (page - 1), numToDisplay * page)
|
||||
.map((slackBotConfig) => {
|
||||
return (
|
||||
<TableRow key={slackBotConfig.id}>
|
||||
<TableCell>
|
||||
<div className="flex gap-x-2">
|
||||
<Link
|
||||
className="cursor-pointer my-auto"
|
||||
href={`/admin/bot/${slackBotConfig.id}`}
|
||||
>
|
||||
<EditIcon />
|
||||
</Link>
|
||||
<div className="my-auto">
|
||||
{slackBotConfig.channel_config.channel_names
|
||||
.map((channel_name) => `#${channel_name}`)
|
||||
.join(", ")}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{slackBotConfig.persona &&
|
||||
!isPersonaASlackBotPersona(slackBotConfig.persona) ? (
|
||||
<Link
|
||||
href={`/admin/personas/${slackBotConfig.persona.id}`}
|
||||
className="text-blue-500 flex"
|
||||
>
|
||||
<FiArrowUpRight className="my-auto mr-1" />
|
||||
{slackBotConfig.persona.name}
|
||||
</Link>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{" "}
|
||||
<div>
|
||||
{slackBotConfig.persona &&
|
||||
slackBotConfig.persona.document_sets.length > 0
|
||||
? slackBotConfig.persona.document_sets
|
||||
.map((documentSet) => documentSet.name)
|
||||
.join(", ")
|
||||
: "-"}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{" "}
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
onClick={async () => {
|
||||
const response = await deleteSlackBotConfig(
|
||||
slackBotConfig.id
|
||||
);
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: `Slack bot config "${slackBotConfig.id}" deleted`,
|
||||
type: "success",
|
||||
});
|
||||
} else {
|
||||
const errorMsg = await response.text();
|
||||
setPopup({
|
||||
message: `Failed to delete Slack bot config - ${errorMsg}`,
|
||||
type: "error",
|
||||
});
|
||||
}
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
|
@@ -72,30 +72,36 @@ export default function Web() {
|
||||
inputType="load_state"
|
||||
formBody={
|
||||
<>
|
||||
<TextFormField name="base_url" label="URL to Index:" />
|
||||
<SelectorFormField
|
||||
name="web_connector_type"
|
||||
label="Scrape Method:"
|
||||
options={[
|
||||
{
|
||||
name: "Recursive",
|
||||
value: "recursive",
|
||||
description:
|
||||
"Recursively index all pages that share the same base URL.",
|
||||
},
|
||||
{
|
||||
name: "Single Page",
|
||||
value: "single",
|
||||
description: "Index only the specified page.",
|
||||
},
|
||||
{
|
||||
name: "Sitemap",
|
||||
value: "sitemap",
|
||||
description:
|
||||
"Assumes the URL to Index points to a Sitemap. Will try and index all pages that are a mentioned in the sitemap.",
|
||||
},
|
||||
]}
|
||||
<TextFormField
|
||||
name="base_url"
|
||||
label="URL to Index:"
|
||||
autoCompleteDisabled={false}
|
||||
/>
|
||||
<div className="w-full">
|
||||
<SelectorFormField
|
||||
name="web_connector_type"
|
||||
label="Scrape Method:"
|
||||
options={[
|
||||
{
|
||||
name: "Recursive",
|
||||
value: "recursive",
|
||||
description:
|
||||
"Recursively index all pages that share the same base URL.",
|
||||
},
|
||||
{
|
||||
name: "Single Page",
|
||||
value: "single",
|
||||
description: "Index only the specified page.",
|
||||
},
|
||||
{
|
||||
name: "Sitemap",
|
||||
value: "sitemap",
|
||||
description:
|
||||
"Assumes the URL to Index points to a Sitemap. Will try and index all pages that are a mentioned in the sitemap.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
validationSchema={Yup.object().shape({
|
||||
|
@@ -1,20 +1,28 @@
|
||||
import { ChangeEvent, FC, useEffect, useRef, useState } from "react";
|
||||
import { ChevronDownIcon } from "./icons/icons";
|
||||
import { FiCheck, FiChevronDown } from "react-icons/fi";
|
||||
import { FaRobot } from "react-icons/fa";
|
||||
|
||||
export interface Option {
|
||||
export interface Option<T> {
|
||||
name: string;
|
||||
value: string;
|
||||
value: T;
|
||||
description?: string;
|
||||
metadata?: { [key: string]: any };
|
||||
}
|
||||
|
||||
interface DropdownProps {
|
||||
options: Option[];
|
||||
export type StringOrNumberOption = Option<string | number>;
|
||||
|
||||
interface DropdownProps<T> {
|
||||
options: Option<T>[];
|
||||
selected: string;
|
||||
onSelect: (selected: Option) => void;
|
||||
onSelect: (selected: Option<T> | null) => void;
|
||||
}
|
||||
|
||||
export const Dropdown = ({ options, selected, onSelect }: DropdownProps) => {
|
||||
export const Dropdown = ({
|
||||
options,
|
||||
selected,
|
||||
onSelect,
|
||||
}: DropdownProps<string | number>) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
@@ -22,7 +30,7 @@ export const Dropdown = ({ options, selected, onSelect }: DropdownProps) => {
|
||||
(option) => option.value === selected
|
||||
)?.name;
|
||||
|
||||
const handleSelect = (option: Option) => {
|
||||
const handleSelect = (option: StringOrNumberOption) => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
};
|
||||
@@ -106,15 +114,15 @@ export const Dropdown = ({ options, selected, onSelect }: DropdownProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const StandardDropdownOption = ({
|
||||
function StandardDropdownOption<T>({
|
||||
index,
|
||||
option,
|
||||
handleSelect,
|
||||
}: {
|
||||
index: number;
|
||||
option: Option;
|
||||
handleSelect: (option: Option) => void;
|
||||
}) => {
|
||||
option: Option<T>;
|
||||
handleSelect: (option: Option<T>) => void;
|
||||
}) {
|
||||
return (
|
||||
<button
|
||||
onClick={() => handleSelect(option)}
|
||||
@@ -131,24 +139,22 @@ const StandardDropdownOption = ({
|
||||
)}
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
interface MultiSelectDropdownProps {
|
||||
options: Option[];
|
||||
onSelect: (selected: Option) => void;
|
||||
itemComponent?: FC<{ option: Option }>;
|
||||
}
|
||||
|
||||
export const SearchMultiSelectDropdown: FC<MultiSelectDropdownProps> = ({
|
||||
export function SearchMultiSelectDropdown({
|
||||
options,
|
||||
onSelect,
|
||||
itemComponent,
|
||||
}) => {
|
||||
}: {
|
||||
options: StringOrNumberOption[];
|
||||
onSelect: (selected: StringOrNumberOption) => void;
|
||||
itemComponent?: FC<{ option: StringOrNumberOption }>;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const dropdownRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const handleSelect = (option: Option) => {
|
||||
const handleSelect = (option: StringOrNumberOption) => {
|
||||
onSelect(option);
|
||||
setIsOpen(false);
|
||||
setSearchTerm(""); // Clear search term after selection
|
||||
@@ -273,7 +279,7 @@ export const SearchMultiSelectDropdown: FC<MultiSelectDropdownProps> = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
export const CustomDropdown = ({
|
||||
children,
|
||||
@@ -316,3 +322,134 @@ export const CustomDropdown = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function DefaultDropdownElement({
|
||||
id,
|
||||
name,
|
||||
description,
|
||||
onSelect,
|
||||
isSelected,
|
||||
isFinal,
|
||||
}: {
|
||||
id: string | number | null;
|
||||
name: string;
|
||||
description?: string;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
isSelected: boolean;
|
||||
isFinal: boolean;
|
||||
}) {
|
||||
console.log(isFinal);
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex
|
||||
px-3
|
||||
text-sm
|
||||
text-gray-200
|
||||
py-2.5
|
||||
select-none
|
||||
cursor-pointer
|
||||
${isFinal ? "" : "border-b border-gray-800"}
|
||||
${
|
||||
isSelected
|
||||
? "bg-dark-tremor-background-muted"
|
||||
: "hover:bg-dark-tremor-background-muted "
|
||||
}
|
||||
`}
|
||||
onClick={() => {
|
||||
onSelect(id);
|
||||
}}
|
||||
>
|
||||
<div>
|
||||
{name}
|
||||
{description && (
|
||||
<div className="text-xs text-dark-tremor-content">{description}</div>
|
||||
)}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="ml-auto mr-1 my-auto">
|
||||
<FiCheck />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function DefaultDropdown({
|
||||
options,
|
||||
selected,
|
||||
onSelect,
|
||||
includeDefault = false,
|
||||
}: {
|
||||
options: StringOrNumberOption[];
|
||||
selected: string | null;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
includeDefault?: boolean;
|
||||
}) {
|
||||
const selectedOption = options.find((option) => option.value === selected);
|
||||
|
||||
return (
|
||||
<CustomDropdown
|
||||
dropdown={
|
||||
<div
|
||||
className={`
|
||||
border
|
||||
border-gray-800
|
||||
rounded-lg
|
||||
flex
|
||||
flex-col
|
||||
max-h-96
|
||||
overflow-y-auto
|
||||
overscroll-contain`}
|
||||
>
|
||||
{includeDefault && (
|
||||
<DefaultDropdownElement
|
||||
key={-1}
|
||||
id={null}
|
||||
name="Default"
|
||||
onSelect={() => {
|
||||
onSelect(null);
|
||||
}}
|
||||
isSelected={selected === null}
|
||||
isFinal={false}
|
||||
/>
|
||||
)}
|
||||
{options.map((option, ind) => {
|
||||
const isSelected = option.value === selected;
|
||||
return (
|
||||
<DefaultDropdownElement
|
||||
key={option.value}
|
||||
id={option.value}
|
||||
name={option.name}
|
||||
description={option.description}
|
||||
onSelect={onSelect}
|
||||
isSelected={isSelected}
|
||||
isFinal={ind === options.length - 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
flex
|
||||
text-sm
|
||||
text-gray-400
|
||||
px-3
|
||||
py-1.5
|
||||
rounded-lg
|
||||
border
|
||||
border-gray-800
|
||||
cursor-pointer
|
||||
hover:bg-dark-tremor-background-muted`}
|
||||
>
|
||||
<p className="text-gray-200 line-clamp-1">
|
||||
{selectedOption?.name ||
|
||||
(includeDefault ? "Default" : "Select an option...")}
|
||||
</p>
|
||||
<FiChevronDown className="my-auto ml-auto" />
|
||||
</div>
|
||||
</CustomDropdown>
|
||||
);
|
||||
}
|
||||
|
@@ -9,9 +9,17 @@ import {
|
||||
} from "formik";
|
||||
import * as Yup from "yup";
|
||||
import { FormBodyBuilder } from "./types";
|
||||
import { Dropdown, Option } from "@/components/Dropdown";
|
||||
import { DefaultDropdown, StringOrNumberOption } from "@/components/Dropdown";
|
||||
import { FiPlus, FiX } from "react-icons/fi";
|
||||
|
||||
export function SectionHeader({
|
||||
children,
|
||||
}: {
|
||||
children: string | JSX.Element;
|
||||
}) {
|
||||
return <div className="mb-4 font-bold text-lg">{children}</div>;
|
||||
}
|
||||
|
||||
export function Label({ children }: { children: string | JSX.Element }) {
|
||||
return (
|
||||
<div className="block font-medium text-base text-gray-200">{children}</div>
|
||||
@@ -212,9 +220,10 @@ export function TextArrayFieldBuilder<T extends Yup.AnyObject>(
|
||||
|
||||
interface SelectorFormFieldProps {
|
||||
name: string;
|
||||
label: string;
|
||||
options: Option[];
|
||||
label?: string;
|
||||
options: StringOrNumberOption[];
|
||||
subtext?: string;
|
||||
includeDefault?: boolean;
|
||||
}
|
||||
|
||||
export function SelectorFormField({
|
||||
@@ -222,24 +231,24 @@ export function SelectorFormField({
|
||||
label,
|
||||
options,
|
||||
subtext,
|
||||
includeDefault = false,
|
||||
}: SelectorFormFieldProps) {
|
||||
const [field] = useField<string>(name);
|
||||
const { setFieldValue } = useFormikContext();
|
||||
|
||||
return (
|
||||
<div className="mb-4">
|
||||
<label className="flex mb-2">
|
||||
<div>
|
||||
{label}
|
||||
{subtext && <p className="text-xs">{subtext}</p>}
|
||||
</div>
|
||||
</label>
|
||||
<div className="mb-4 dark">
|
||||
{label && <Label>{label}</Label>}
|
||||
{subtext && <SubLabel>{subtext}</SubLabel>}
|
||||
|
||||
<Dropdown
|
||||
options={options}
|
||||
selected={field.value}
|
||||
onSelect={(selected) => setFieldValue(name, selected.value)}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<DefaultDropdown
|
||||
options={options}
|
||||
selected={field.value}
|
||||
onSelect={(selected) => setFieldValue(name, selected)}
|
||||
includeDefault={includeDefault}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<ErrorMessage
|
||||
name={name}
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { Persona } from "@/app/admin/personas/interfaces";
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
@@ -301,7 +303,7 @@ export interface ChannelConfig {
|
||||
|
||||
export interface SlackBotConfig {
|
||||
id: number;
|
||||
document_sets: DocumentSet[];
|
||||
persona: Persona | null;
|
||||
channel_config: ChannelConfig;
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,11 @@ export function buildUrl(path: string) {
|
||||
return `${INTERNAL_URL}/${path}`;
|
||||
}
|
||||
|
||||
export function fetchSS(url: string, options?: RequestInit) {
|
||||
export function fetchSS(
|
||||
url: string,
|
||||
options?: RequestInit,
|
||||
addRandomTimestamp: boolean = false
|
||||
) {
|
||||
const init = options || {
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
@@ -20,5 +24,11 @@ export function fetchSS(url: string, options?: RequestInit) {
|
||||
},
|
||||
};
|
||||
|
||||
// add a random timestamp to force NextJS to refetch rather than
|
||||
// used cached data
|
||||
if (addRandomTimestamp) {
|
||||
const timestamp = Date.now();
|
||||
url = `${url}?u=${timestamp}`;
|
||||
}
|
||||
return fetch(buildUrl(url), init);
|
||||
}
|
||||
|
Reference in New Issue
Block a user