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,38 +1,36 @@
"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}
>
<div
className="bg-gray-800 rounded-lg border border-gray-700 shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
>
<Formik <Formik
initialValues={{ initialValues={{
channel_names: existingSlackBotConfig channel_names: existingSlackBotConfig
@ -45,8 +43,7 @@ export const SlackBotCreationForm = ({
existingSlackBotConfig?.channel_config?.answer_filters || [] existingSlackBotConfig?.channel_config?.answer_filters || []
).includes("questionmark_prefilter"), ).includes("questionmark_prefilter"),
respond_tag_only: respond_tag_only:
existingSlackBotConfig?.channel_config?.respond_tag_only || existingSlackBotConfig?.channel_config?.respond_tag_only || false,
false,
respond_team_member_list: respond_team_member_list:
existingSlackBotConfig?.channel_config existingSlackBotConfig?.channel_config
?.respond_team_member_list || ([] as string[]), ?.respond_team_member_list || ([] as string[]),
@ -73,8 +70,7 @@ export const SlackBotCreationForm = ({
channel_names: values.channel_names.filter( channel_names: values.channel_names.filter(
(channelName) => channelName !== "" (channelName) => channelName !== ""
), ),
respond_team_member_list: respond_team_member_list: values.respond_team_member_list.filter(
values.respond_team_member_list.filter(
(teamMemberEmail) => teamMemberEmail !== "" (teamMemberEmail) => teamMemberEmail !== ""
), ),
}; };
@ -90,13 +86,7 @@ export const SlackBotCreationForm = ({
} }
formikHelpers.setSubmitting(false); formikHelpers.setSubmitting(false);
if (response.ok) { if (response.ok) {
setPopup({ router.push(`/admin/bot?u=${Date.now()}`);
message: isUpdate
? "Successfully updated DanswerBot config!"
: "Successfully created DanswerBot config!",
type: "success",
});
onClose();
} else { } else {
const responseJson = await response.json(); const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message; const errorMsg = responseJson.detail || responseJson.message;
@ -111,15 +101,10 @@ export const SlackBotCreationForm = ({
> >
{({ isSubmitting, values }) => ( {({ isSubmitting, values }) => (
<Form> <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"> <div className="px-6 pb-6">
<TextArrayField <TextArrayField
name="channel_names" name="channel_names"
label="Channel Names:" label="Channel Names"
values={values} values={values}
subtext={ subtext={
<div> <div>
@ -134,28 +119,28 @@ export const SlackBotCreationForm = ({
</div> </div>
} }
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<BooleanFormField <BooleanFormField
name="answer_validity_check_enabled" name="answer_validity_check_enabled"
label="Hide Non-Answers" label="Hide Non-Answers"
subtext="If set, will only answer questions that the model determines it can answer" subtext="If set, will only answer questions that the model determines it can answer"
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<BooleanFormField <BooleanFormField
name="questionmark_prefilter_enabled" name="questionmark_prefilter_enabled"
label="Only respond to questions" label="Only respond to questions"
subtext="If set, will only respond to messages that contain a question mark" subtext="If set, will only respond to messages that contain a question mark"
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<BooleanFormField <BooleanFormField
name="respond_tag_only" name="respond_tag_only"
label="Respond to @DanswerBot Only" label="Respond to @DanswerBot Only"
subtext="If set, DanswerBot will only respond when directly tagged" subtext="If set, DanswerBot will only respond when directly tagged"
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<TextArrayField <TextArrayField
name="respond_team_member_list" name="respond_team_member_list"
label="Team Members Emails:" label="Team Members Emails"
subtext={`If specified, DanswerBot responses will only be 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
@ -164,18 +149,18 @@ export const SlackBotCreationForm = ({
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>
<p className="font-medium">Document Sets:</p> <Label>Document Sets</Label>
<div className="text-xs"> <SubLabel>
The document sets that DanswerBot should search The document sets that DanswerBot should search
through. If left blank, DanswerBot will search through. If left blank, DanswerBot will search through
through all documents. all documents.
</div> </SubLabel>
</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) => {
@ -208,9 +193,7 @@ export const SlackBotCreationForm = ({
} }
}} }}
> >
<div className="my-auto"> <div className="my-auto">{documentSet.name}</div>
{documentSet.name}
</div>
</div> </div>
); );
})} })}
@ -218,7 +201,7 @@ export const SlackBotCreationForm = ({
</div> </div>
)} )}
/> />
<div className="border-t border-gray-600 py-2" /> <Divider />
<div className="flex"> <div className="flex">
<button <button
type="submit" type="submit"
@ -236,8 +219,7 @@ export const SlackBotCreationForm = ({
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </Card>
</div>
</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,15 +25,7 @@ export const SlackBotTokensForm = ({
existingTokens, existingTokens,
}: SlackBotTokensFormProps) => { }: SlackBotTokensFormProps) => {
return ( return (
<div> <Card>
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={onClose}
>
<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 <Formik
initialValues={existingTokens || { app_token: "", bot_token: "" }} initialValues={existingTokens || { app_token: "", bot_token: "" }}
validationSchema={Yup.object().shape({ validationSchema={Yup.object().shape({
@ -60,7 +53,6 @@ export const SlackBotTokensForm = ({
> >
{({ isSubmitting }) => ( {({ isSubmitting }) => (
<Form> <Form>
<h2 className="text-lg font-bold mb-3">Set Slack Bot Tokens</h2>
<TextFormField <TextFormField
name="bot_token" name="bot_token"
label="Slack Bot Token" label="Slack Bot Token"
@ -87,8 +79,6 @@ export const SlackBotTokensForm = ({
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </Card>
</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,30 +188,69 @@ 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 ? (
<div className="mt-3">
<SlackBotTokensForm <SlackBotTokensForm
onClose={() => refreshSlackBotTokens()} onClose={() => refreshSlackBotTokens()}
setPopup={setPopup} 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 && (
<div className="mt-3">
<SlackBotTokensForm <SlackBotTokensForm
onClose={() => { onClose={() => {
refreshSlackBotTokens(); refreshSlackBotTokens();
@ -266,46 +259,35 @@ const Main = () => {
setPopup={setPopup} setPopup={setPopup}
existingTokens={slackBotTokens} 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>
{slackBotConfigs.length > 0 && (
<div className="mt-8">
<SlackBotConfigsTable <SlackBotConfigsTable
slackBotConfigs={slackBotConfigs} slackBotConfigs={slackBotConfigs}
documentSets={documentSets}
refresh={refreshSlackBotConfigs} refresh={refreshSlackBotConfigs}
setPopup={setPopup} setPopup={setPopup}
/> />
</div>
{slackBotConfigModalIsOpen && (
<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,33 +10,45 @@ 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}
@ -44,17 +56,19 @@ export const TextFormField = ({
` `
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
className="my-auto w-10 h-10 cursor-pointer hover:bg-gray-800 rounded p-2"
onClick={() => arrayHelpers.remove(index)} onClick={() => arrayHelpers.remove(index)}
className="h-8 my-auto" />
> </div>
Remove
</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
}, },