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 * 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
@ -45,8 +43,7 @@ export const SlackBotCreationForm = ({
existingSlackBotConfig?.channel_config?.answer_filters || []
).includes("questionmark_prefilter"),
respond_tag_only:
existingSlackBotConfig?.channel_config?.respond_tag_only ||
false,
existingSlackBotConfig?.channel_config?.respond_tag_only || false,
respond_team_member_list:
existingSlackBotConfig?.channel_config
?.respond_team_member_list || ([] as string[]),
@ -73,8 +70,7 @@ export const SlackBotCreationForm = ({
channel_names: values.channel_names.filter(
(channelName) => channelName !== ""
),
respond_team_member_list:
values.respond_team_member_list.filter(
respond_team_member_list: values.respond_team_member_list.filter(
(teamMemberEmail) => teamMemberEmail !== ""
),
};
@ -90,13 +86,7 @@ export const SlackBotCreationForm = ({
}
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
message: isUpdate
? "Successfully updated DanswerBot config!"
: "Successfully created DanswerBot config!",
type: "success",
});
onClose();
router.push(`/admin/bot?u=${Date.now()}`);
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
@ -111,15 +101,10 @@ export const SlackBotCreationForm = ({
>
{({ 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:"
label="Channel Names"
values={values}
subtext={
<div>
@ -134,28 +119,28 @@ export const SlackBotCreationForm = ({
</div>
}
/>
<div className="border-t border-gray-600 py-2" />
<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"
/>
<div className="border-t border-gray-600 py-2" />
<Divider />
<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" />
<Divider />
<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" />
<Divider />
<TextArrayField
name="respond_team_member_list"
label="Team Members Emails:"
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
@ -164,18 +149,18 @@ export const SlackBotCreationForm = ({
out the occasional incorrect answer.`}
values={values}
/>
<div className="border-t border-gray-600 py-2" />
<Divider />
<FieldArray
name="document_sets"
render={(arrayHelpers: ArrayHelpers) => (
<div>
<div>
<p className="font-medium">Document Sets:</p>
<div className="text-xs">
<Label>Document Sets</Label>
<SubLabel>
The document sets that DanswerBot should search
through. If left blank, DanswerBot will search
through all documents.
</div>
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) => {
@ -208,9 +193,7 @@ export const SlackBotCreationForm = ({
}
}}
>
<div className="my-auto">
{documentSet.name}
</div>
<div className="my-auto">{documentSet.name}</div>
</div>
);
})}
@ -218,7 +201,7 @@ export const SlackBotCreationForm = ({
</div>
)}
/>
<div className="border-t border-gray-600 py-2" />
<Divider />
<div className="flex">
<button
type="submit"
@ -236,8 +219,7 @@ export const SlackBotCreationForm = ({
</Form>
)}
</Formik>
</div>
</div>
</Card>
</div>
);
};

View File

@ -11,6 +11,7 @@ import {
setSlackBotTokens,
updateSlackBotConfig,
} from "./lib";
import { Card } from "@tremor/react";
interface SlackBotTokensFormProps {
onClose: () => void;
@ -24,15 +25,7 @@ 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}
>
<div
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
>
<Card>
<Formik
initialValues={existingTokens || { app_token: "", bot_token: "" }}
validationSchema={Yup.object().shape({
@ -60,7 +53,6 @@ export const SlackBotTokensForm = ({
>
{({ isSubmitting }) => (
<Form>
<h2 className="text-lg font-bold mb-3">Set Slack Bot Tokens</h2>
<TextFormField
name="bot_token"
label="Slack Bot Token"
@ -87,8 +79,6 @@ export const SlackBotTokensForm = ({
</Form>
)}
</Formik>
</div>
</div>
</div>
</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,30 +188,69 @@ 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 ? (
<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 && (
<div className="mt-3">
<SlackBotTokensForm
onClose={() => {
refreshSlackBotTokens();
@ -266,46 +259,35 @@ const Main = () => {
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>
{slackBotConfigs.length > 0 && (
<div className="mt-8">
<SlackBotConfigsTable
slackBotConfigs={slackBotConfigs}
documentSets={documentSets}
refresh={refreshSlackBotConfigs}
setPopup={setPopup}
/>
{slackBotConfigModalIsOpen && (
<SlackBotCreationForm
documentSets={documentSets}
onClose={() => {
refreshSlackBotConfigs();
setSlackBotConfigModalIsOpen(false);
}}
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,33 +10,45 @@ 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}
@ -44,17 +56,19 @@ export const TextFormField = ({
`
border
text-gray-200
border-gray-300
border-gray-600
rounded
w-full
py-2
px-3
mt-1
` + (disabled ? " bg-slate-900" : " bg-slate-700")
${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"
<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)}
className="h-8 my-auto"
>
Remove
</Button>
/>
</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
},