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 * 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 {
BooleanFormField,
Label,
SubLabel,
TextArrayField,
} from "@/components/admin/connectors/Field";
import { createSlackBotConfig, updateSlackBotConfig } from "./lib";
import { Card, Divider } from "@tremor/react";
import { useRouter } from "next/navigation";
interface SetCreationPopupProps {
onClose: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
documentSets: DocumentSet[];
existingSlackBotConfig?: SlackBotConfig;
}
export const SlackBotCreationForm = ({
onClose,
setPopup,
documentSets,
existingSlackBotConfig,
}: SetCreationPopupProps) => {
const isUpdate = existingSlackBotConfig !== undefined;
const { popup, setPopup } = usePopup();
const router = useRouter();
return (
<div>
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-10 overflow-y-auto overscroll-contain"
onClick={onClose}
>
<div
className="bg-gray-800 rounded-lg border border-gray-700 shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
<div className="dark">
<Card>
{popup}
<Formik
initialValues={{
channel_names: existingSlackBotConfig
? existingSlackBotConfig.channel_config.channel_names
: ([] 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
initialValues={{
channel_names: existingSlackBotConfig
? existingSlackBotConfig.channel_config.channel_names
: ([] 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) {
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
{({ isSubmitting, values }) => (
<Form>
<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>
}
/>
<Divider />
<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"
subtext={`If specified, DanswerBot responses will only be
visible to members in this list. This is
useful if you want DanswerBot to operate in an
"assistant" mode, where it helps the team members find
answers, but let's them build on top of DanswerBot's response / throw
out the occasional incorrect answer.`}
values={values}
/>
<div className="border-t border-gray-600 py-2" />
<FieldArray
name="document_sets"
render={(arrayHelpers: ArrayHelpers) => (
values={values}
/>
<Divider />
<FieldArray
name="document_sets"
render={(arrayHelpers: ArrayHelpers) => (
<div>
<div>
<div>
<p className="font-medium">Document Sets:</p>
<div className="text-xs">
The document sets that DanswerBot should search
through. If left blank, DanswerBot will search
through all documents.
</div>
</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={
`
<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={
`
px-3
py-1
rounded-lg
@ -196,48 +181,45 @@ export const SlackBotCreationForm = ({
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-gray-600"
: " bg-gray-900 hover:bg-gray-700")
(isSelected
? " bg-gray-600"
: " bg-gray-900 hover:bg-gray-700")
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(documentSet.id);
}
onClick={() => {
if (isSelected) {
arrayHelpers.remove(ind);
} else {
arrayHelpers.push(documentSet.id);
}
}}
>
<div className="my-auto">
{documentSet.name}
</div>
</div>
);
})}
</div>
}}
>
<div className="my-auto">{documentSet.name}</div>
</div>
);
})}
</div>
)}
/>
<div className="border-t border-gray-600 py-2" />
<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"
}
>
{isUpdate ? "Update!" : "Create!"}
</button>
</div>
</div>
)}
/>
<Divider />
<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"
}
>
{isUpdate ? "Update!" : "Create!"}
</button>
</div>
</Form>
)}
</Formik>
</div>
</div>
</div>
</Form>
)}
</Formik>
</Card>
</div>
);
};

View File

@ -11,6 +11,7 @@ import {
setSlackBotTokens,
updateSlackBotConfig,
} from "./lib";
import { Card } from "@tremor/react";
interface SlackBotTokensFormProps {
onClose: () => void;
@ -24,71 +25,60 @@ export const SlackBotTokensForm = ({
existingTokens,
}: SlackBotTokensFormProps) => {
return (
<div>
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={onClose}
<Card>
<Formik
initialValues={existingTokens || { app_token: "", bot_token: "" }}
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
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
>
<Formik
initialValues={existingTokens || { app_token: "", bot_token: "" }}
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",
});
}
}}
>
{({ 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>
{({ isSubmitting }) => (
<Form>
<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>
</Card>
);
};

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";
import { Button } from "@/components/Button";
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 { DocumentSet, SlackBotConfig } from "@/lib/types";
import { SlackBotConfig } from "@/lib/types";
import { useState } from "react";
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { SlackBotCreationForm } from "./SlackBotConfigCreationForm";
import { deleteSlackBotConfig } from "./lib";
import { SlackBotTokensForm } from "./SlackBotTokensForm";
import { useDocumentSets } from "../documents/sets/hooks";
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 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 = ({
slackBotConfigs,
documentSets,
refresh,
setPopup,
}: DocumentFeedbackTableProps) => {
}: {
slackBotConfigs: SlackBotConfig[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) => {
const [page, setPage] = useState(1);
// sort by name for consistent ordering
@ -118,12 +79,12 @@ const SlackBotConfigsTable = ({
return {
channels: (
<div className="flex gap-x-2">
<EditRow
existingSlackBotConfig={slackBotConfig}
setPopup={setPopup}
refreshSlackBotConfigs={refresh}
documentSets={documentSets}
/>
<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}`)
@ -207,8 +168,6 @@ const SlackBotConfigsTable = ({
};
const Main = () => {
const [slackBotConfigModalIsOpen, setSlackBotConfigModalIsOpen] =
useState(false);
const [slackBotTokensModalIsOpen, setSlackBotTokensModalIsOpen] =
useState(false);
const { popup, setPopup } = usePopup();
@ -218,15 +177,10 @@ const Main = () => {
error: slackBotConfigsError,
refreshSlackBotConfigs,
} = useSlackBotConfigs();
const {
data: documentSets,
isLoading: isDocumentSetsLoading,
error: documentSetsError,
} = useDocumentSets();
const { data: slackBotTokens, refreshSlackBotTokens } = useSlackBotTokens();
if (isSlackBotConfigsLoading || isDocumentSetsLoading) {
if (isSlackBotConfigsLoading) {
return <ThreeDotsLoader />;
}
@ -234,78 +188,106 @@ const Main = () => {
return <div>Error: {slackBotConfigsError}</div>;
}
if (documentSetsError || !documentSets) {
return <div>Error: {documentSetsError}</div>;
}
return (
<div className="mb-8">
<div className="mb-8 dark">
{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 ? (
<SlackBotTokensForm
onClose={() => refreshSlackBotTokens()}
setPopup={setPopup}
/>
<div className="mt-3">
<SlackBotTokensForm
onClose={() => refreshSlackBotTokens()}
setPopup={setPopup}
/>
</div>
) : (
<>
<div className="text-sm italic">Tokens saved!</div>
<Text className="italic mt-3">Tokens saved!</Text>
<Button
onClick={() => setSlackBotTokensModalIsOpen(true)}
onClick={() => {
setSlackBotTokensModalIsOpen(!slackBotTokensModalIsOpen);
console.log(slackBotTokensModalIsOpen);
}}
variant="secondary"
size="xs"
className="mt-2"
icon={slackBotTokensModalIsOpen ? FiChevronUp : FiChevronDown}
>
Edit Tokens
{slackBotTokensModalIsOpen ? "Hide" : "Edit Tokens"}
</Button>
{slackBotTokensModalIsOpen && (
<SlackBotTokensForm
onClose={() => {
refreshSlackBotTokens();
setSlackBotTokensModalIsOpen(false);
}}
setPopup={setPopup}
existingTokens={slackBotTokens}
/>
<div className="mt-3">
<SlackBotTokensForm
onClose={() => {
refreshSlackBotTokens();
setSlackBotTokensModalIsOpen(false);
}}
setPopup={setPopup}
existingTokens={slackBotTokens}
/>
</div>
)}
</>
)}
{slackBotTokens && (
<>
<h2 className="text-lg font-bold mb-2 mt-4">
Step 2: Setup DanswerBot
</h2>
<div className="text-sm mb-3">
<Title className="mb-2 mt-4">Step 2: Setup DanswerBot</Title>
<Text className="mb-3">
Configure Danswer to automatically answer questions in Slack
channels.
</div>
channels. By default, Danswer only responds in channels where a
configuration is setup unless it is explicitly tagged.
</Text>
<div className="mb-2"></div>
<div className="flex mb-3">
<Button
className="ml-2 my-auto"
onClick={() => setSlackBotConfigModalIsOpen(true)}
>
New Slack Bot
<Link className="flex mb-3" href="/admin/bot/new">
<Button className="my-auto" variant="secondary" size="xs">
New Slack Bot Configuration
</Button>
</div>
</Link>
<SlackBotConfigsTable
slackBotConfigs={slackBotConfigs}
documentSets={documentSets}
refresh={refreshSlackBotConfigs}
setPopup={setPopup}
/>
{slackBotConfigModalIsOpen && (
<SlackBotCreationForm
documentSets={documentSets}
onClose={() => {
refreshSlackBotConfigs();
setSlackBotConfigModalIsOpen(false);
}}
setPopup={setPopup}
/>
{slackBotConfigs.length > 0 && (
<div className="mt-8">
<SlackBotConfigsTable
slackBotConfigs={slackBotConfigs}
refresh={refreshSlackBotConfigs}
setPopup={setPopup}
/>
</div>
)}
</>
)}
@ -315,7 +297,7 @@ const Main = () => {
const Page = () => {
return (
<div>
<div className="container mx-auto">
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="Slack Bot Configuration"

View File

@ -18,6 +18,10 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { Persona } from "./interfaces";
import Link from "next/link";
import { useEffect, useState } from "react";
import {
BooleanFormField,
TextFormField,
} from "@/components/admin/connectors/Field";
function SectionHeader({ children }: { children: string | JSX.Element }) {
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>;
}
// 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({
existingPersona,
documentSets,
@ -226,14 +135,14 @@ export function PersonaEditor({
<div className="pb-6">
<SectionHeader>Who am I?</SectionHeader>
<PersonaTextInput
<TextFormField
name="name"
label="Name"
disabled={isUpdate}
subtext="Users will be able to select this Persona based on this name."
/>
<PersonaTextInput
<TextFormField
name="description"
label="Description"
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>
<PersonaTextInput
<TextFormField
name="system_prompt"
label="System Prompt"
isTextArea={true}
@ -256,7 +165,7 @@ export function PersonaEditor({
}}
/>
<PersonaTextInput
<TextFormField
name="task_prompt"
label="Task Prompt"
isTextArea={true}
@ -352,7 +261,7 @@ export function PersonaEditor({
<SectionHeader>[Advanced] Retrieval Customization</SectionHeader>
<PersonaTextInput
<TextFormField
name="num_chunks"
label="Number of Chunks"
subtext={
@ -376,7 +285,7 @@ export function PersonaEditor({
}}
/>
<PersonaBooleanInput
<BooleanFormField
name="apply_llm_relevance_filter"
label="Apply LLM Relevance Filter"
subtext={

View File

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

View File

@ -23,13 +23,15 @@ export default async function Page() {
const personas = (await personaResponse.json()) as Persona[];
return (
<div>
<div className="dark">
<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
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">
<li>
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 {
ArrayHelpers,
ErrorMessage,
@ -10,51 +10,65 @@ import {
import * as Yup from "yup";
import { FormBodyBuilder } from "./types";
import { Dropdown, Option } from "@/components/Dropdown";
import { FiPlus, FiX } from "react-icons/fi";
interface TextFormFieldProps {
name: string;
label: string;
subtext?: string;
placeholder?: string;
type?: string;
disabled?: boolean;
autoCompleteDisabled?: boolean;
export function Label({ children }: { children: string | JSX.Element }) {
return (
<div className="block font-medium text-base text-gray-200">{children}</div>
);
}
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,
label,
subtext,
placeholder,
onChange,
type = "text",
isTextArea = false,
disabled = false,
autoCompleteDisabled = false,
}: TextFormFieldProps) => {
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 htmlFor={name} className="block font-medium">
{label}
</label>
{subtext && <p className="text-xs mb-1">{subtext}</p>}
<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-300
rounded
w-full
py-2
px-3
mt-1
` + (disabled ? " bg-slate-900" : " bg-slate-700")
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}
@ -63,7 +77,7 @@ export const TextFormField = ({
/>
</div>
);
};
}
interface BooleanFormFieldProps {
name: string;
@ -79,10 +93,14 @@ export const BooleanFormField = ({
return (
<div className="mb-4">
<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>
<p className="font-medium">{label}</p>
{subtext && <p className="text-xs">{subtext}</p>}
<Label>{label}</Label>
{subtext && <SubLabel>{subtext}</SubLabel>}
</div>
</label>
@ -111,11 +129,9 @@ export function TextArrayField<T extends Yup.AnyObject>({
type,
}: TextArrayFieldProps<T>) {
return (
<div className="mb-4">
<label htmlFor={name} className="block font-medium">
{label}
</label>
{subtext && <p className="text-xs">{subtext}</p>}
<div className="mb-4 dark">
<Label>{label}</Label>
{subtext && <SubLabel>{subtext}</SubLabel>}
<FieldArray
name={name}
@ -130,17 +146,26 @@ export function TextArrayField<T extends Yup.AnyObject>({
type={type}
name={`${name}.${index}`}
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
autoComplete="off"
/>
<Button
type="button"
onClick={() => arrayHelpers.remove(index)}
className="h-8 my-auto"
>
Remove
</Button>
<div className="my-auto">
<FiX
className="my-auto w-10 h-10 cursor-pointer hover:bg-gray-800 rounded p-2"
onClick={() => arrayHelpers.remove(index)}
/>
</div>
</div>
<ErrorMessage
name={`${name}.${index}`}
@ -149,12 +174,16 @@ export function TextArrayField<T extends Yup.AnyObject>({
/>
</div>
))}
<Button
type="button"
onClick={() => {
arrayHelpers.push("");
}}
className="mt-3"
variant="secondary"
size="xs"
type="button"
icon={FiPlus}
>
Add New
</Button>

View File

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