From f7172612e178bcd4cf379fb622cc142649ac4845 Mon Sep 17 00:00:00 2001 From: Weves Date: Mon, 4 Dec 2023 16:26:34 -0800 Subject: [PATCH] Allow persona usage for Slack bots --- .../slack/handlers/handle_message.py | 10 +- backend/danswer/db/chat.py | 7 +- backend/danswer/db/slack_bot_config.py | 72 +++--- backend/danswer/direct_qa/answer_question.py | 37 ++- backend/danswer/server/manage/models.py | 23 +- backend/danswer/server/manage/slack_bot.py | 72 ++++-- .../admin/bot/SlackBotConfigCreationForm.tsx | 212 ++++++++++++----- web/src/app/admin/bot/[id]/page.tsx | 20 +- web/src/app/admin/bot/lib.ts | 11 +- web/src/app/admin/bot/new/page.tsx | 17 +- web/src/app/admin/bot/page.tsx | 217 ++++++++---------- web/src/app/admin/connectors/web/page.tsx | 52 +++-- web/src/components/Dropdown.tsx | 179 +++++++++++++-- web/src/components/admin/connectors/Field.tsx | 39 ++-- web/src/lib/types.ts | 4 +- web/src/lib/utilsSS.ts | 12 +- 16 files changed, 671 insertions(+), 313 deletions(-) diff --git a/backend/danswer/danswerbot/slack/handlers/handle_message.py b/backend/danswer/danswerbot/slack/handlers/handle_message.py index d526ed55fe0c..01d8122bd046 100644 --- a/backend/danswer/danswerbot/slack/handlers/handle_message.py +++ b/backend/danswer/danswerbot/slack/handlers/handle_message.py @@ -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 diff --git a/backend/danswer/db/chat.py b/backend/danswer/db/chat.py index 9f5ef32eda0e..2e132bdfac48 100644 --- a/backend/danswer/db/chat.py +++ b/backend/danswer/db/chat.py @@ -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 diff --git a/backend/danswer/db/slack_bot_config.py b/backend/danswer/db/slack_bot_config.py index 67ff0aaef2f0..2e1e0b79b486 100644 --- a/backend/danswer/db/slack_bot_config.py +++ b/backend/danswer/db/slack_bot_config.py @@ -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() diff --git a/backend/danswer/direct_qa/answer_question.py b/backend/danswer/direct_qa/answer_question.py index 4c74d4a97e10..d3e1f057d100 100644 --- a/backend/danswer/direct_qa/answer_question.py +++ b/backend/danswer/direct_qa/answer_question.py @@ -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)) diff --git a/backend/danswer/server/manage/models.py b/backend/danswer/server/manage/models.py index aace8ed1c928..9cc49e62c8d8 100644 --- a/backend/danswer/server/manage/models.py +++ b/backend/danswer/server/manage/models.py @@ -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 diff --git a/backend/danswer/server/manage/slack_bot.py b/backend/danswer/server/manage/slack_bot.py index 5d88c9f66466..0461e0cc5702 100644 --- a/backend/danswer/server/manage/slack_bot.py +++ b/backend/danswer/server/manage/slack_bot.py @@ -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 diff --git a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx index f469ab95cf9d..c4c18d7ffbdf 100644 --- a/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx +++ b/web/src/app/admin/bot/SlackBotConfigCreationForm.tsx @@ -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 (
@@ -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 }) => (
+ The Basics + + + When should DanswerBot respond? + - - - - ( -
-
- - - The document sets that DanswerBot should search - through. If left blank, DanswerBot will search through - all documents. - -
-
- {documentSets.map((documentSet) => { - const ind = values.document_sets.indexOf( - documentSet.id - ); - let isSelected = ind !== -1; - return ( -
+ + [Optional] Data Sources and Prompts + + + Use either a Persona or Document Sets to control how + DanswerBot answers. + +
+
    +
  • + You should use a Persona if you also want to customize + the prompt and retrieval settings. +
  • +
  • + You should use Document Sets if you just want to control + which documents DanswerBot uses as references. +
  • +
+
+ + NOTE: 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. + +
+ + setUsingPersonas(index === 1)} + > + + Document Sets + Personas + + + + ( +
+
+ + The document sets that DanswerBot should search + through. If left blank, DanswerBot will search + through all documents. + +
+
+ {documentSets.map((documentSet) => { + const ind = values.document_sets.indexOf( + documentSet.id + ); + let isSelected = ind !== -1; + return ( +
{ - if (isSelected) { - arrayHelpers.remove(ind); - } else { - arrayHelpers.push(documentSet.id); - } - }} - > -
{documentSet.name}
+ (isSelected + ? " bg-gray-600" + : " bg-gray-900 hover:bg-gray-700") + } + onClick={() => { + if (isSelected) { + arrayHelpers.remove(ind); + } else { + arrayHelpers.push(documentSet.id); + } + }} + > +
+ {documentSet.name} +
+
+ ); + })}
- ); +
+ )} + /> +
+ + { + return { + name: persona.name, + value: persona.id, + }; })} -
-
- )} - /> + includeDefault={true} + /> + + + + +
); -}; - -interface MultiSelectDropdownProps { - options: Option[]; - onSelect: (selected: Option) => void; - itemComponent?: FC<{ option: Option }>; } -export const SearchMultiSelectDropdown: FC = ({ +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(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 = ({ )}
); -}; +} export const CustomDropdown = ({ children, @@ -316,3 +322,134 @@ export const CustomDropdown = ({
); }; + +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 ( +
{ + onSelect(id); + }} + > +
+ {name} + {description && ( +
{description}
+ )} +
+ {isSelected && ( +
+ +
+ )} +
+ ); +} + +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 ( + + {includeDefault && ( + { + onSelect(null); + }} + isSelected={selected === null} + isFinal={false} + /> + )} + {options.map((option, ind) => { + const isSelected = option.value === selected; + return ( + + ); + })} +
+ } + > +
+

+ {selectedOption?.name || + (includeDefault ? "Default" : "Select an option...")} +

+ +
+ + ); +} diff --git a/web/src/components/admin/connectors/Field.tsx b/web/src/components/admin/connectors/Field.tsx index e3481021082e..e1de3f0ac8e4 100644 --- a/web/src/components/admin/connectors/Field.tsx +++ b/web/src/components/admin/connectors/Field.tsx @@ -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
{children}
; +} + export function Label({ children }: { children: string | JSX.Element }) { return (
{children}
@@ -212,9 +220,10 @@ export function TextArrayFieldBuilder( 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(name); const { setFieldValue } = useFormikContext(); return ( -
- +
+ {label && } + {subtext && {subtext}} - setFieldValue(name, selected.value)} - /> +
+ setFieldValue(name, selected)} + includeDefault={includeDefault} + /> +