Make Slack Bot setup UI more similar to Persona setup

This commit is contained in:
Weves
2023-12-03 23:28:41 -08:00
committed by Chris Weaver
parent 651de071f7
commit 5607fdcddd
11 changed files with 1271 additions and 1161 deletions

1358
web/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,193 +1,178 @@
"use client";
import { ArrayHelpers, FieldArray, Form, Formik } from "formik"; import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import * as Yup from "yup"; import * as Yup from "yup";
import { PopupSpec } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { DocumentSet, SlackBotConfig } from "@/lib/types"; import { DocumentSet, SlackBotConfig } from "@/lib/types";
import { import {
BooleanFormField, BooleanFormField,
Label,
SubLabel,
TextArrayField, TextArrayField,
} from "@/components/admin/connectors/Field"; } from "@/components/admin/connectors/Field";
import { createSlackBotConfig, updateSlackBotConfig } from "./lib"; import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
import { Card, Divider } from "@tremor/react";
import { useRouter } from "next/navigation";
interface SetCreationPopupProps { interface SetCreationPopupProps {
onClose: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
documentSets: DocumentSet[]; documentSets: DocumentSet[];
existingSlackBotConfig?: SlackBotConfig; existingSlackBotConfig?: SlackBotConfig;
} }
export const SlackBotCreationForm = ({ export const SlackBotCreationForm = ({
onClose,
setPopup,
documentSets, documentSets,
existingSlackBotConfig, existingSlackBotConfig,
}: SetCreationPopupProps) => { }: SetCreationPopupProps) => {
const isUpdate = existingSlackBotConfig !== undefined; const isUpdate = existingSlackBotConfig !== undefined;
const { popup, setPopup } = usePopup();
const router = useRouter();
return ( return (
<div> <div className="dark">
<div <Card>
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-10 overflow-y-auto overscroll-contain" {popup}
onClick={onClose} <Formik
> initialValues={{
<div channel_names: existingSlackBotConfig
className="bg-gray-800 rounded-lg border border-gray-700 shadow-lg relative w-1/2 text-sm" ? existingSlackBotConfig.channel_config.channel_names
onClick={(event) => event.stopPropagation()} : ([] as string[]),
answer_validity_check_enabled: (
existingSlackBotConfig?.channel_config?.answer_filters || []
).includes("well_answered_postfilter"),
questionmark_prefilter_enabled: (
existingSlackBotConfig?.channel_config?.answer_filters || []
).includes("questionmark_prefilter"),
respond_tag_only:
existingSlackBotConfig?.channel_config?.respond_tag_only || false,
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[]),
}}
validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string()),
answer_validity_check_enabled: Yup.boolean().required(),
questionmark_prefilter_enabled: Yup.boolean().required(),
respond_tag_only: Yup.boolean().required(),
respond_team_member_list: Yup.array().of(Yup.string()).required(),
document_sets: Yup.array().of(Yup.number()),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
// remove empty channel names
const cleanedValues = {
...values,
channel_names: values.channel_names.filter(
(channelName) => channelName !== ""
),
respond_team_member_list: values.respond_team_member_list.filter(
(teamMemberEmail) => teamMemberEmail !== ""
),
};
let response;
if (isUpdate) {
response = await updateSlackBotConfig(
existingSlackBotConfig.id,
cleanedValues
);
} else {
response = await createSlackBotConfig(cleanedValues);
}
formikHelpers.setSubmitting(false);
if (response.ok) {
router.push(`/admin/bot?u=${Date.now()}`);
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: isUpdate
? `Error updating DanswerBot config - ${errorMsg}`
: `Error creating DanswerBot config - ${errorMsg}`,
type: "error",
});
}
}}
> >
<Formik {({ isSubmitting, values }) => (
initialValues={{ <Form>
channel_names: existingSlackBotConfig <div className="px-6 pb-6">
? existingSlackBotConfig.channel_config.channel_names <TextArrayField
: ([] as string[]), name="channel_names"
answer_validity_check_enabled: ( label="Channel Names"
existingSlackBotConfig?.channel_config?.answer_filters || [] values={values}
).includes("well_answered_postfilter"), subtext={
questionmark_prefilter_enabled: ( <div>
existingSlackBotConfig?.channel_config?.answer_filters || [] The names of the Slack channels you want this
).includes("questionmark_prefilter"), configuration to apply to. For example,
respond_tag_only: &apos;#ask-danswer&apos;.
existingSlackBotConfig?.channel_config?.respond_tag_only || <br />
false, <br />
respond_team_member_list: <i>NOTE</i>: you still need to add DanswerBot to the
existingSlackBotConfig?.channel_config channel(s) in Slack itself. Setting this config will not
?.respond_team_member_list || ([] as string[]), auto-add the bot to the channel.
document_sets: existingSlackBotConfig </div>
? existingSlackBotConfig.document_sets.map( }
(documentSet) => documentSet.id />
) <Divider />
: ([] as number[]), <BooleanFormField
}} name="answer_validity_check_enabled"
validationSchema={Yup.object().shape({ label="Hide Non-Answers"
channel_names: Yup.array().of(Yup.string()), subtext="If set, will only answer questions that the model determines it can answer"
answer_validity_check_enabled: Yup.boolean().required(), />
questionmark_prefilter_enabled: Yup.boolean().required(), <Divider />
respond_tag_only: Yup.boolean().required(), <BooleanFormField
respond_team_member_list: Yup.array().of(Yup.string()).required(), name="questionmark_prefilter_enabled"
document_sets: Yup.array().of(Yup.number()), label="Only respond to questions"
})} subtext="If set, will only respond to messages that contain a question mark"
onSubmit={async (values, formikHelpers) => { />
formikHelpers.setSubmitting(true); <Divider />
<BooleanFormField
// remove empty channel names name="respond_tag_only"
const cleanedValues = { label="Respond to @DanswerBot Only"
...values, subtext="If set, DanswerBot will only respond when directly tagged"
channel_names: values.channel_names.filter( />
(channelName) => channelName !== "" <Divider />
), <TextArrayField
respond_team_member_list: name="respond_team_member_list"
values.respond_team_member_list.filter( label="Team Members Emails"
(teamMemberEmail) => teamMemberEmail !== "" subtext={`If specified, DanswerBot responses will only be
),
};
let response;
if (isUpdate) {
response = await updateSlackBotConfig(
existingSlackBotConfig.id,
cleanedValues
);
} else {
response = await createSlackBotConfig(cleanedValues);
}
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
message: isUpdate
? "Successfully updated DanswerBot config!"
: "Successfully created DanswerBot config!",
type: "success",
});
onClose();
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: isUpdate
? `Error updating DanswerBot config - ${errorMsg}`
: `Error creating DanswerBot config - ${errorMsg}`,
type: "error",
});
}
}}
>
{({ isSubmitting, values }) => (
<Form>
<h2 className="text-xl font-bold mb-3 border-b border-gray-600 pt-4 pb-3 bg-gray-700 px-6">
{isUpdate
? "Update a DanswerBot Config"
: "Create a new DanswerBot Config"}
</h2>
<div className="px-6 pb-6">
<TextArrayField
name="channel_names"
label="Channel Names:"
values={values}
subtext={
<div>
The names of the Slack channels you want this
configuration to apply to. For example,
&apos;#ask-danswer&apos;.
<br />
<br />
<i>NOTE</i>: you still need to add DanswerBot to the
channel(s) in Slack itself. Setting this config will not
auto-add the bot to the channel.
</div>
}
/>
<div className="border-t border-gray-600 py-2" />
<BooleanFormField
name="answer_validity_check_enabled"
label="Hide Non-Answers"
subtext="If set, will only answer questions that the model determines it can answer"
/>
<div className="border-t border-gray-600 py-2" />
<BooleanFormField
name="questionmark_prefilter_enabled"
label="Only respond to questions"
subtext="If set, will only respond to messages that contain a question mark"
/>
<div className="border-t border-gray-600 py-2" />
<BooleanFormField
name="respond_tag_only"
label="Respond to @DanswerBot Only"
subtext="If set, DanswerBot will only respond when directly tagged"
/>
<div className="border-t border-gray-600 py-2" />
<TextArrayField
name="respond_team_member_list"
label="Team Members Emails:"
subtext={`If specified, DanswerBot responses will only be
visible to members in this list. This is visible to members in this list. This is
useful if you want DanswerBot to operate in an useful if you want DanswerBot to operate in an
"assistant" mode, where it helps the team members find "assistant" mode, where it helps the team members find
answers, but let's them build on top of DanswerBot's response / throw answers, but let's them build on top of DanswerBot's response / throw
out the occasional incorrect answer.`} out the occasional incorrect answer.`}
values={values} values={values}
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<FieldArray <FieldArray
name="document_sets" name="document_sets"
render={(arrayHelpers: ArrayHelpers) => ( render={(arrayHelpers: ArrayHelpers) => (
<div>
<div> <div>
<div> <Label>Document Sets</Label>
<p className="font-medium">Document Sets:</p> <SubLabel>
<div className="text-xs"> The document sets that DanswerBot should search
The document sets that DanswerBot should search through. If left blank, DanswerBot will search through
through. If left blank, DanswerBot will search all documents.
through all documents. </SubLabel>
</div> </div>
</div> <div className="mb-3 mt-2 flex gap-2 flex-wrap">
<div className="mb-3 mt-2 flex gap-2 flex-wrap"> {documentSets.map((documentSet) => {
{documentSets.map((documentSet) => { const ind = values.document_sets.indexOf(
const ind = values.document_sets.indexOf( documentSet.id
documentSet.id );
); let isSelected = ind !== -1;
let isSelected = ind !== -1; return (
return ( <div
<div key={documentSet.id}
key={documentSet.id} className={
className={ `
`
px-3 px-3
py-1 py-1
rounded-lg rounded-lg
@@ -196,48 +181,45 @@ export const SlackBotCreationForm = ({
w-fit w-fit
flex flex
cursor-pointer ` + cursor-pointer ` +
(isSelected (isSelected
? " bg-gray-600" ? " bg-gray-600"
: " bg-gray-900 hover:bg-gray-700") : " bg-gray-900 hover:bg-gray-700")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(documentSet.id);
} }
onClick={() => { }}
if (isSelected) { >
arrayHelpers.remove(ind); <div className="my-auto">{documentSet.name}</div>
} else { </div>
arrayHelpers.push(documentSet.id); );
} })}
}}
>
<div className="my-auto">
{documentSet.name}
</div>
</div>
);
})}
</div>
</div> </div>
)} </div>
/> )}
<div className="border-t border-gray-600 py-2" /> />
<div className="flex"> <Divider />
<button <div className="flex">
type="submit" <button
disabled={isSubmitting} type="submit"
className={ disabled={isSubmitting}
"bg-slate-500 hover:bg-slate-700 text-white " + className={
"font-bold py-2 px-4 rounded focus:outline-none " + "bg-slate-500 hover:bg-slate-700 text-white " +
"focus:shadow-outline w-full max-w-sm mx-auto" "font-bold py-2 px-4 rounded focus:outline-none " +
} "focus:shadow-outline w-full max-w-sm mx-auto"
> }
{isUpdate ? "Update!" : "Create!"} >
</button> {isUpdate ? "Update!" : "Create!"}
</div> </button>
</div> </div>
</Form> </div>
)} </Form>
</Formik> )}
</div> </Formik>
</div> </Card>
</div> </div>
); );
}; };

View File

@@ -11,6 +11,7 @@ import {
setSlackBotTokens, setSlackBotTokens,
updateSlackBotConfig, updateSlackBotConfig,
} from "./lib"; } from "./lib";
import { Card } from "@tremor/react";
interface SlackBotTokensFormProps { interface SlackBotTokensFormProps {
onClose: () => void; onClose: () => void;
@@ -24,71 +25,60 @@ export const SlackBotTokensForm = ({
existingTokens, existingTokens,
}: SlackBotTokensFormProps) => { }: SlackBotTokensFormProps) => {
return ( return (
<div> <Card>
<div <Formik
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" initialValues={existingTokens || { app_token: "", bot_token: "" }}
onClick={onClose} validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string().required()),
document_sets: Yup.array().of(Yup.number()),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
const response = await setSlackBotTokens(values);
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
message: "Successfully set Slack tokens!",
type: "success",
});
onClose();
} else {
const errorMsg = await response.text();
setPopup({
message: `Error setting Slack tokens - ${errorMsg}`,
type: "error",
});
}
}}
> >
<div {({ isSubmitting }) => (
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm" <Form>
onClick={(event) => event.stopPropagation()} <TextFormField
> name="bot_token"
<Formik label="Slack Bot Token"
initialValues={existingTokens || { app_token: "", bot_token: "" }} type="password"
validationSchema={Yup.object().shape({ />
channel_names: Yup.array().of(Yup.string().required()), <TextFormField
document_sets: Yup.array().of(Yup.number()), name="app_token"
})} label="Slack App Token"
onSubmit={async (values, formikHelpers) => { type="password"
formikHelpers.setSubmitting(true); />
const response = await setSlackBotTokens(values); <div className="flex">
formikHelpers.setSubmitting(false); <button
if (response.ok) { type="submit"
setPopup({ disabled={isSubmitting}
message: "Successfully set Slack tokens!", className={
type: "success", "bg-slate-500 hover:bg-slate-700 text-white " +
}); "font-bold py-2 px-4 rounded focus:outline-none " +
onClose(); "focus:shadow-outline w-full max-w-sm mx-auto"
} else { }
const errorMsg = await response.text(); >
setPopup({ Set Tokens
message: `Error setting Slack tokens - ${errorMsg}`, </button>
type: "error", </div>
}); </Form>
} )}
}} </Formik>
> </Card>
{({ isSubmitting }) => (
<Form>
<h2 className="text-lg font-bold mb-3">Set Slack Bot Tokens</h2>
<TextFormField
name="bot_token"
label="Slack Bot Token"
type="password"
/>
<TextFormField
name="app_token"
label="Slack App Token"
type="password"
/>
<div className="flex">
<button
type="submit"
disabled={isSubmitting}
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full max-w-sm mx-auto"
}
>
Set Tokens
</button>
</div>
</Form>
)}
</Formik>
</div>
</div>
</div>
); );
}; };

View File

@@ -0,0 +1,71 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { CPUIcon } from "@/components/icons/icons";
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import { Text } from "@tremor/react";
import { BackButton } from "@/components/BackButton";
async function Page({ params }: { params: { id: string } }) {
const tasks = [
fetchSS("/manage/admin/slack-bot/config"),
fetchSS("/manage/document-set"),
];
const [slackBotsResponse, documentSetsResponse] = await Promise.all(tasks);
if (!slackBotsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch slack bots - ${await slackBotsResponse.text()}`}
/>
);
}
const allSlackBotConfigs =
(await slackBotsResponse.json()) as SlackBotConfig[];
const slackBotConfig = allSlackBotConfigs.find(
(config) => config.id.toString() === params.id
);
if (!slackBotConfig) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Did not find Slack Bot config with ID: ${params.id}`}
/>
);
}
if (!documentSetsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch document sets - ${await documentSetsResponse.text()}`}
/>
);
}
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
return (
<div className="container mx-auto dark">
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="Edit Slack Bot Config"
/>
<Text className="mb-8">
Edit the existing configuration below! This config will determine how
DanswerBot behaves in the specified channels.
</Text>
<SlackBotCreationForm
documentSets={documentSets}
existingSlackBotConfig={slackBotConfig}
/>
</div>
);
}
export default Page;

View File

@@ -0,0 +1,40 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { CPUIcon } from "@/components/icons/icons";
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { Text } from "@tremor/react";
async function Page() {
const documentSetsResponse = await fetchSS("/manage/document-set");
if (!documentSetsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch document sets - ${await documentSetsResponse.text()}`}
/>
);
}
const documentSets = (await documentSetsResponse.json()) as DocumentSet[];
return (
<div className="container mx-auto dark">
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="New Slack Bot Config"
/>
<Text className="mb-8">
Define a new configuration below! This config will determine how
DanswerBot behaves in the specified channels.
</Text>
<SlackBotCreationForm documentSets={documentSets} />
</div>
);
}
export default Page;

View File

@@ -1,70 +1,31 @@
"use client"; "use client";
import { Button } from "@/components/Button";
import { ThreeDotsLoader } from "@/components/Loading"; import { ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector"; import { PageSelector } from "@/components/PageSelector";
import { BasicTable } from "@/components/admin/connectors/BasicTable"; import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons"; import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons";
import { DocumentSet, SlackBotConfig } from "@/lib/types"; import { SlackBotConfig } from "@/lib/types";
import { useState } from "react"; import { useState } from "react";
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks"; import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { SlackBotCreationForm } from "./SlackBotConfigCreationForm";
import { deleteSlackBotConfig } from "./lib"; import { deleteSlackBotConfig } from "./lib";
import { SlackBotTokensForm } from "./SlackBotTokensForm"; import { SlackBotTokensForm } from "./SlackBotTokensForm";
import { useDocumentSets } from "../documents/sets/hooks";
import { AdminPageTitle } from "@/components/admin/Title"; import { AdminPageTitle } from "@/components/admin/Title";
import { Button, Text, Title } from "@tremor/react";
import { FiChevronDown, FiChevronUp } from "react-icons/fi";
import Link from "next/link";
const numToDisplay = 50; const numToDisplay = 50;
const EditRow = ({
existingSlackBotConfig,
setPopup,
documentSets,
refreshSlackBotConfigs,
}: {
existingSlackBotConfig: SlackBotConfig;
setPopup: (popupSpec: PopupSpec | null) => void;
documentSets: DocumentSet[];
refreshSlackBotConfigs: () => void;
}) => {
const [isEditPopupOpen, setEditPopupOpen] = useState(false);
return (
<>
{isEditPopupOpen && (
<SlackBotCreationForm
onClose={() => {
setEditPopupOpen(false);
refreshSlackBotConfigs();
}}
setPopup={setPopup}
documentSets={documentSets}
existingSlackBotConfig={existingSlackBotConfig}
/>
)}
<div
className="cursor-pointer my-auto"
onClick={() => setEditPopupOpen(true)}
>
<EditIcon />
</div>
</>
);
};
interface DocumentFeedbackTableProps {
slackBotConfigs: SlackBotConfig[];
documentSets: DocumentSet[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}
const SlackBotConfigsTable = ({ const SlackBotConfigsTable = ({
slackBotConfigs, slackBotConfigs,
documentSets,
refresh, refresh,
setPopup, setPopup,
}: DocumentFeedbackTableProps) => { }: {
slackBotConfigs: SlackBotConfig[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) => {
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
// sort by name for consistent ordering // sort by name for consistent ordering
@@ -118,12 +79,12 @@ const SlackBotConfigsTable = ({
return { return {
channels: ( channels: (
<div className="flex gap-x-2"> <div className="flex gap-x-2">
<EditRow <Link
existingSlackBotConfig={slackBotConfig} className="cursor-pointer my-auto"
setPopup={setPopup} href={`/admin/bot/${slackBotConfig.id}`}
refreshSlackBotConfigs={refresh} >
documentSets={documentSets} <EditIcon />
/> </Link>
<div className="my-auto"> <div className="my-auto">
{slackBotConfig.channel_config.channel_names {slackBotConfig.channel_config.channel_names
.map((channel_name) => `#${channel_name}`) .map((channel_name) => `#${channel_name}`)
@@ -207,8 +168,6 @@ const SlackBotConfigsTable = ({
}; };
const Main = () => { const Main = () => {
const [slackBotConfigModalIsOpen, setSlackBotConfigModalIsOpen] =
useState(false);
const [slackBotTokensModalIsOpen, setSlackBotTokensModalIsOpen] = const [slackBotTokensModalIsOpen, setSlackBotTokensModalIsOpen] =
useState(false); useState(false);
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
@@ -218,15 +177,10 @@ const Main = () => {
error: slackBotConfigsError, error: slackBotConfigsError,
refreshSlackBotConfigs, refreshSlackBotConfigs,
} = useSlackBotConfigs(); } = useSlackBotConfigs();
const {
data: documentSets,
isLoading: isDocumentSetsLoading,
error: documentSetsError,
} = useDocumentSets();
const { data: slackBotTokens, refreshSlackBotTokens } = useSlackBotTokens(); const { data: slackBotTokens, refreshSlackBotTokens } = useSlackBotTokens();
if (isSlackBotConfigsLoading || isDocumentSetsLoading) { if (isSlackBotConfigsLoading) {
return <ThreeDotsLoader />; return <ThreeDotsLoader />;
} }
@@ -234,78 +188,106 @@ const Main = () => {
return <div>Error: {slackBotConfigsError}</div>; return <div>Error: {slackBotConfigsError}</div>;
} }
if (documentSetsError || !documentSets) {
return <div>Error: {documentSetsError}</div>;
}
return ( return (
<div className="mb-8"> <div className="mb-8 dark">
{popup} {popup}
<h2 className="text-lg font-bold mb-2">Step 1: Configure Slack Tokens</h2> <Text className="mb-2">
Setup a Slack bot that connects to Danswer. Once setup, you will be able
to ask questions to Danswer directly from Slack. Additionally, you can:
</Text>
<div className="text-dark-tremor-content text-sm mb-2">
<ul className="list-disc mt-2 ml-4">
<li>
Setup DanswerBot to automatically answer questions in certain
channels.
</li>
<li>
Choose which document sets DanswerBot should answer from, depending
on the channel the question is being asked.
</li>
<li>
Directly message DanswerBot to search just as you would in the web
UI.
</li>
</ul>
</div>
<Text className="mb-6">
Follow the{" "}
<a
className="text-blue-500"
href="https://docs.danswer.dev/slack_bot_setup"
target="_blank"
>
guide{" "}
</a>
found in the Danswer documentation to get started!
</Text>
<Title>Step 1: Configure Slack Tokens</Title>
{!slackBotTokens ? ( {!slackBotTokens ? (
<SlackBotTokensForm <div className="mt-3">
onClose={() => refreshSlackBotTokens()} <SlackBotTokensForm
setPopup={setPopup} onClose={() => refreshSlackBotTokens()}
/> setPopup={setPopup}
/>
</div>
) : ( ) : (
<> <>
<div className="text-sm italic">Tokens saved!</div> <Text className="italic mt-3">Tokens saved!</Text>
<Button <Button
onClick={() => setSlackBotTokensModalIsOpen(true)} onClick={() => {
setSlackBotTokensModalIsOpen(!slackBotTokensModalIsOpen);
console.log(slackBotTokensModalIsOpen);
}}
variant="secondary"
size="xs"
className="mt-2" className="mt-2"
icon={slackBotTokensModalIsOpen ? FiChevronUp : FiChevronDown}
> >
Edit Tokens {slackBotTokensModalIsOpen ? "Hide" : "Edit Tokens"}
</Button> </Button>
{slackBotTokensModalIsOpen && ( {slackBotTokensModalIsOpen && (
<SlackBotTokensForm <div className="mt-3">
onClose={() => { <SlackBotTokensForm
refreshSlackBotTokens(); onClose={() => {
setSlackBotTokensModalIsOpen(false); refreshSlackBotTokens();
}} setSlackBotTokensModalIsOpen(false);
setPopup={setPopup} }}
existingTokens={slackBotTokens} setPopup={setPopup}
/> existingTokens={slackBotTokens}
/>
</div>
)} )}
</> </>
)} )}
{slackBotTokens && ( {slackBotTokens && (
<> <>
<h2 className="text-lg font-bold mb-2 mt-4"> <Title className="mb-2 mt-4">Step 2: Setup DanswerBot</Title>
Step 2: Setup DanswerBot <Text className="mb-3">
</h2>
<div className="text-sm mb-3">
Configure Danswer to automatically answer questions in Slack Configure Danswer to automatically answer questions in Slack
channels. channels. By default, Danswer only responds in channels where a
</div> configuration is setup unless it is explicitly tagged.
</Text>
<div className="mb-2"></div> <div className="mb-2"></div>
<div className="flex mb-3"> <Link className="flex mb-3" href="/admin/bot/new">
<Button <Button className="my-auto" variant="secondary" size="xs">
className="ml-2 my-auto" New Slack Bot Configuration
onClick={() => setSlackBotConfigModalIsOpen(true)}
>
New Slack Bot
</Button> </Button>
</div> </Link>
<SlackBotConfigsTable {slackBotConfigs.length > 0 && (
slackBotConfigs={slackBotConfigs} <div className="mt-8">
documentSets={documentSets} <SlackBotConfigsTable
refresh={refreshSlackBotConfigs} slackBotConfigs={slackBotConfigs}
setPopup={setPopup} refresh={refreshSlackBotConfigs}
/> setPopup={setPopup}
/>
{slackBotConfigModalIsOpen && ( </div>
<SlackBotCreationForm
documentSets={documentSets}
onClose={() => {
refreshSlackBotConfigs();
setSlackBotConfigModalIsOpen(false);
}}
setPopup={setPopup}
/>
)} )}
</> </>
)} )}
@@ -315,7 +297,7 @@ const Main = () => {
const Page = () => { const Page = () => {
return ( return (
<div> <div className="container mx-auto">
<AdminPageTitle <AdminPageTitle
icon={<CPUIcon size={32} />} icon={<CPUIcon size={32} />}
title="Slack Bot Configuration" title="Slack Bot Configuration"

View File

@@ -18,6 +18,10 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { Persona } from "./interfaces"; import { Persona } from "./interfaces";
import Link from "next/link"; import Link from "next/link";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import {
BooleanFormField,
TextFormField,
} from "@/components/admin/connectors/Field";
function SectionHeader({ children }: { children: string | JSX.Element }) { function SectionHeader({ children }: { children: string | JSX.Element }) {
return <div className="mb-4 font-bold text-lg">{children}</div>; return <div className="mb-4 font-bold text-lg">{children}</div>;
@@ -33,101 +37,6 @@ function SubLabel({ children }: { children: string | JSX.Element }) {
return <div className="text-sm text-gray-300 mb-2">{children}</div>; return <div className="text-sm text-gray-300 mb-2">{children}</div>;
} }
// TODO: make this the default text input across all forms
function PersonaTextInput({
name,
label,
subtext,
placeholder,
onChange,
type = "text",
isTextArea = false,
disabled = false,
autoCompleteDisabled = true,
}: {
name: string;
label: string;
subtext?: string | JSX.Element;
placeholder?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
type?: string;
isTextArea?: boolean;
disabled?: boolean;
autoCompleteDisabled?: boolean;
}) {
return (
<div className="mb-4">
<Label>{label}</Label>
{subtext && <SubLabel>{subtext}</SubLabel>}
<Field
as={isTextArea ? "textarea" : "input"}
type={type}
name={name}
id={name}
className={
`
border
text-gray-200
border-gray-600
rounded
w-full
py-2
px-3
mt-1
${isTextArea ? " h-28" : ""}
` + (disabled ? " bg-gray-900" : " bg-gray-800")
}
disabled={disabled}
placeholder={placeholder}
autoComplete={autoCompleteDisabled ? "off" : undefined}
{...(onChange ? { onChange } : {})}
/>
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
);
}
function PersonaBooleanInput({
name,
label,
subtext,
}: {
name: string;
label: string;
subtext?: string | JSX.Element;
}) {
return (
<div className="mb-4">
<Label>{label}</Label>
{subtext && <SubLabel>{subtext}</SubLabel>}
<Field
type="checkbox"
name={name}
id={name}
className={`
ml-2
border
text-gray-200
border-gray-600
rounded
py-2
px-3
mt-1
`}
/>
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-sm mt-1"
/>
</div>
);
}
export function PersonaEditor({ export function PersonaEditor({
existingPersona, existingPersona,
documentSets, documentSets,
@@ -226,14 +135,14 @@ export function PersonaEditor({
<div className="pb-6"> <div className="pb-6">
<SectionHeader>Who am I?</SectionHeader> <SectionHeader>Who am I?</SectionHeader>
<PersonaTextInput <TextFormField
name="name" name="name"
label="Name" label="Name"
disabled={isUpdate} disabled={isUpdate}
subtext="Users will be able to select this Persona based on this name." subtext="Users will be able to select this Persona based on this name."
/> />
<PersonaTextInput <TextFormField
name="description" name="description"
label="Description" label="Description"
subtext="Provide a short descriptions which gives users a hint as to what they should use this Persona for." subtext="Provide a short descriptions which gives users a hint as to what they should use this Persona for."
@@ -243,7 +152,7 @@ export function PersonaEditor({
<SectionHeader>Customize my response style</SectionHeader> <SectionHeader>Customize my response style</SectionHeader>
<PersonaTextInput <TextFormField
name="system_prompt" name="system_prompt"
label="System Prompt" label="System Prompt"
isTextArea={true} isTextArea={true}
@@ -256,7 +165,7 @@ export function PersonaEditor({
}} }}
/> />
<PersonaTextInput <TextFormField
name="task_prompt" name="task_prompt"
label="Task Prompt" label="Task Prompt"
isTextArea={true} isTextArea={true}
@@ -352,7 +261,7 @@ export function PersonaEditor({
<SectionHeader>[Advanced] Retrieval Customization</SectionHeader> <SectionHeader>[Advanced] Retrieval Customization</SectionHeader>
<PersonaTextInput <TextFormField
name="num_chunks" name="num_chunks"
label="Number of Chunks" label="Number of Chunks"
subtext={ subtext={
@@ -376,7 +285,7 @@ export function PersonaEditor({
}} }}
/> />
<PersonaBooleanInput <BooleanFormField
name="apply_llm_relevance_filter" name="apply_llm_relevance_filter"
label="Apply LLM Relevance Filter" label="Apply LLM Relevance Filter"
subtext={ subtext={

View File

@@ -1,4 +1,3 @@
import { FaRobot } from "react-icons/fa";
import { PersonaEditor } from "../PersonaEditor"; import { PersonaEditor } from "../PersonaEditor";
import { fetchSS } from "@/lib/utilsSS"; import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout"; import { ErrorCallout } from "@/components/ErrorCallout";
@@ -6,6 +5,7 @@ import { DocumentSet } from "@/lib/types";
import { RobotIcon } from "@/components/icons/icons"; import { RobotIcon } from "@/components/icons/icons";
import { BackButton } from "@/components/BackButton"; import { BackButton } from "@/components/BackButton";
import { Card } from "@tremor/react"; import { Card } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
export default async function Page() { export default async function Page() {
const documentSetsResponse = await fetchSS("/manage/document-set"); const documentSetsResponse = await fetchSS("/manage/document-set");
@@ -24,10 +24,11 @@ export default async function Page() {
return ( return (
<div className="dark"> <div className="dark">
<BackButton /> <BackButton />
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<RobotIcon size={32} /> <AdminPageTitle
<h1 className="text-3xl font-bold pl-2">Create a New Persona</h1> title="Create a New Persona"
</div> icon={<RobotIcon size={32} />}
/>
<Card> <Card>
<PersonaEditor documentSets={documentSets} /> <PersonaEditor documentSets={documentSets} />

View File

@@ -23,13 +23,15 @@ export default async function Page() {
const personas = (await personaResponse.json()) as Persona[]; const personas = (await personaResponse.json()) as Persona[];
return ( return (
<div> <div className="dark">
<AdminPageTitle icon={<RobotIcon size={32} />} title="Personas" /> <AdminPageTitle icon={<RobotIcon size={32} />} title="Personas" />
<div className="text-gray-300 text-sm mb-2"> <Text className="mb-2">
Personas are a way to build custom search/question-answering experiences Personas are a way to build custom search/question-answering experiences
for different use cases. for different use cases.
<p className="mt-2">They allow you to customize:</p> </Text>
<Text className="mt-2">They allow you to customize:</Text>
<div className="text-dark-tremor-content text-sm">
<ul className="list-disc mt-2 ml-4"> <ul className="list-disc mt-2 ml-4">
<li> <li>
The prompt used by your LLM of choice to respond to the user query The prompt used by your LLM of choice to respond to the user query

View File

@@ -1,4 +1,4 @@
import { Button } from "@/components/Button"; import { Button } from "@tremor/react";
import { import {
ArrayHelpers, ArrayHelpers,
ErrorMessage, ErrorMessage,
@@ -10,51 +10,65 @@ import {
import * as Yup from "yup"; import * as Yup from "yup";
import { FormBodyBuilder } from "./types"; import { FormBodyBuilder } from "./types";
import { Dropdown, Option } from "@/components/Dropdown"; import { Dropdown, Option } from "@/components/Dropdown";
import { FiPlus, FiX } from "react-icons/fi";
interface TextFormFieldProps { export function Label({ children }: { children: string | JSX.Element }) {
name: string; return (
label: string; <div className="block font-medium text-base text-gray-200">{children}</div>
subtext?: string; );
placeholder?: string;
type?: string;
disabled?: boolean;
autoCompleteDisabled?: boolean;
} }
export const TextFormField = ({ export function SubLabel({ children }: { children: string | JSX.Element }) {
return <div className="text-sm text-gray-300 mb-2">{children}</div>;
}
export function TextFormField({
name, name,
label, label,
subtext, subtext,
placeholder, placeholder,
onChange,
type = "text", type = "text",
isTextArea = false,
disabled = false, disabled = false,
autoCompleteDisabled = false, autoCompleteDisabled = true,
}: TextFormFieldProps) => { }: {
name: string;
label: string;
subtext?: string | JSX.Element;
placeholder?: string;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
type?: string;
isTextArea?: boolean;
disabled?: boolean;
autoCompleteDisabled?: boolean;
}) {
return ( return (
<div className="mb-4"> <div className="mb-4">
<label htmlFor={name} className="block font-medium"> <Label>{label}</Label>
{label} {subtext && <SubLabel>{subtext}</SubLabel>}
</label>
{subtext && <p className="text-xs mb-1">{subtext}</p>}
<Field <Field
as={isTextArea ? "textarea" : "input"}
type={type} type={type}
name={name} name={name}
id={name} id={name}
className={ className={
` `
border border
text-gray-200 text-gray-200
border-gray-300 border-gray-600
rounded rounded
w-full w-full
py-2 py-2
px-3 px-3
mt-1 mt-1
` + (disabled ? " bg-slate-900" : " bg-slate-700") ${isTextArea ? " h-28" : ""}
` + (disabled ? " bg-gray-900" : " bg-gray-800")
} }
disabled={disabled} disabled={disabled}
placeholder={placeholder} placeholder={placeholder}
autoComplete={autoCompleteDisabled ? "off" : undefined} autoComplete={autoCompleteDisabled ? "off" : undefined}
{...(onChange ? { onChange } : {})}
/> />
<ErrorMessage <ErrorMessage
name={name} name={name}
@@ -63,7 +77,7 @@ export const TextFormField = ({
/> />
</div> </div>
); );
}; }
interface BooleanFormFieldProps { interface BooleanFormFieldProps {
name: string; name: string;
@@ -79,10 +93,14 @@ export const BooleanFormField = ({
return ( return (
<div className="mb-4"> <div className="mb-4">
<label className="flex text-sm"> <label className="flex text-sm">
<Field name={name} type="checkbox" className="mx-3 px-5" /> <Field
name={name}
type="checkbox"
className="mx-3 px-5 w-3.5 h-3.5 my-auto"
/>
<div> <div>
<p className="font-medium">{label}</p> <Label>{label}</Label>
{subtext && <p className="text-xs">{subtext}</p>} {subtext && <SubLabel>{subtext}</SubLabel>}
</div> </div>
</label> </label>
@@ -111,11 +129,9 @@ export function TextArrayField<T extends Yup.AnyObject>({
type, type,
}: TextArrayFieldProps<T>) { }: TextArrayFieldProps<T>) {
return ( return (
<div className="mb-4"> <div className="mb-4 dark">
<label htmlFor={name} className="block font-medium"> <Label>{label}</Label>
{label} {subtext && <SubLabel>{subtext}</SubLabel>}
</label>
{subtext && <p className="text-xs">{subtext}</p>}
<FieldArray <FieldArray
name={name} name={name}
@@ -130,17 +146,26 @@ export function TextArrayField<T extends Yup.AnyObject>({
type={type} type={type}
name={`${name}.${index}`} name={`${name}.${index}`}
id={name} id={name}
className="border bg-slate-700 text-gray-200 border-gray-300 rounded w-full py-2 px-3 mr-2" className={`
border
text-gray-200
border-gray-600
rounded
w-full
py-2
px-3
bg-gray-800
mr-4
`}
// Disable autocomplete since the browser doesn't know how to handle an array of text fields // Disable autocomplete since the browser doesn't know how to handle an array of text fields
autoComplete="off" autoComplete="off"
/> />
<Button <div className="my-auto">
type="button" <FiX
onClick={() => arrayHelpers.remove(index)} className="my-auto w-10 h-10 cursor-pointer hover:bg-gray-800 rounded p-2"
className="h-8 my-auto" onClick={() => arrayHelpers.remove(index)}
> />
Remove </div>
</Button>
</div> </div>
<ErrorMessage <ErrorMessage
name={`${name}.${index}`} name={`${name}.${index}`}
@@ -149,12 +174,16 @@ export function TextArrayField<T extends Yup.AnyObject>({
/> />
</div> </div>
))} ))}
<Button <Button
type="button"
onClick={() => { onClick={() => {
arrayHelpers.push(""); arrayHelpers.push("");
}} }}
className="mt-3" className="mt-3"
variant="secondary"
size="xs"
type="button"
icon={FiPlus}
> >
Add New Add New
</Button> </Button>

View File

@@ -73,9 +73,9 @@ module.exports = {
DEFAULT: "#1f2937", // gray-800 DEFAULT: "#1f2937", // gray-800
}, },
content: { content: {
subtle: "#4b5563", // gray-600 subtle: "#6b7280", // gray-500
DEFAULT: "#9ca3af", // gray-400 DEFAULT: "#d1d5db", // gray-300
emphasis: "#e5e7eb", // gray-200 emphasis: "#f3f4f6", // gray-100
strong: "#f9fafb", // gray-50 strong: "#f9fafb", // gray-50
inverted: "#000000", // black inverted: "#000000", // black
}, },