multiple slackbot support (#3077)

* multiple slackbot support

* app_id + tenant_id key

* removed kv store stuff

* fixed up mypy and migration

* got frontend working for multiple slack bots

* some frontend stuff

* alembic fix

* might be valid

* refactor dun

* alembic stuff

* temp frontend stuff

* alembic stuff

* maybe fixed alembic

* maybe dis fix

* im getting mad

* api names changed

* tested

* almost done

* done

* routing nonsense

* done!

* done!!

* fr done

* doneski

* fix alembic migration

* getting mad again

* PLEASE IM BEGGING YOU
This commit is contained in:
hagen-danswer
2024-11-19 17:49:43 -08:00
committed by GitHub
parent b712877701
commit 9209fc804b
59 changed files with 2422 additions and 1026 deletions

View File

@@ -1,23 +0,0 @@
import { errorHandlingFetcher } from "@/lib/fetcher";
import { SlackBotConfig, SlackBotTokens } from "@/lib/types";
import useSWR, { mutate } from "swr";
export const useSlackBotConfigs = () => {
const url = "/api/manage/admin/slack-bot/config";
const swrResponse = useSWR<SlackBotConfig[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBotConfigs: () => mutate(url),
};
};
export const useSlackBotTokens = () => {
const url = "/api/manage/admin/slack-bot/tokens";
const swrResponse = useSWR<SlackBotTokens>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBotTokens: () => mutate(url),
};
};

View File

@@ -1,65 +0,0 @@
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 {
FetchAssistantsResponse,
fetchAssistantsSS,
} from "@/lib/assistants/fetchAssistantsSS";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
async function Page() {
const tasks = [fetchSS("/manage/document-set"), fetchAssistantsSS()];
const [
documentSetsResponse,
[assistants, assistantsFetchError],
standardAnswerCategoriesResponse,
] = (await Promise.all(tasks)) as [
Response,
FetchAssistantsResponse,
Response,
];
const eeStandardAnswerCategoryResponse =
await getStandardAnswerCategoriesIfEE();
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[];
if (assistantsFetchError) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch assistants - ${assistantsFetchError}`}
/>
);
}
return (
<div className="container mx-auto">
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="New Slack Bot Config"
/>
<SlackBotCreationForm
documentSets={documentSets}
personas={assistants}
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
/>
</div>
);
}
export default Page;

View File

@@ -1,304 +0,0 @@
"use client";
import { ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector";
import { EditIcon, SlackIcon, TrashIcon } from "@/components/icons/icons";
import { SlackBotConfig } from "@/lib/types";
import { useState } from "react";
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { deleteSlackBotConfig, isPersonaASlackBotPersona } from "./lib";
import { SlackBotTokensForm } from "./SlackBotTokensForm";
import { AdminPageTitle } from "@/components/admin/Title";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import { FiArrowUpRight, FiChevronDown, FiChevronUp } from "react-icons/fi";
import Link from "next/link";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { ErrorCallout } from "@/components/ErrorCallout";
import { Button } from "@/components/ui/button";
const numToDisplay = 50;
const SlackBotConfigsTable = ({
slackBotConfigs,
refresh,
setPopup,
}: {
slackBotConfigs: SlackBotConfig[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) => {
const [page, setPage] = useState(1);
// sort by name for consistent ordering
slackBotConfigs.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
return (
<div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Channels</TableHead>
<TableHead>Assistant</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackBotConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackBotConfig) => {
return (
<TableRow key={slackBotConfig.id}>
<TableCell>
<div className="flex gap-x-2">
<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}`)
.join(", ")}
</div>
</div>
</TableCell>
<TableCell>
{slackBotConfig.persona &&
!isPersonaASlackBotPersona(slackBotConfig.persona) ? (
<Link
href={`/admin/assistants/${slackBotConfig.persona.id}`}
className="text-blue-500 flex"
>
<FiArrowUpRight className="my-auto mr-1" />
{slackBotConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
{" "}
<div>
{slackBotConfig.persona &&
slackBotConfig.persona.document_sets.length > 0
? slackBotConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell>
{" "}
<div
className="cursor-pointer"
onClick={async () => {
const response = await deleteSlackBotConfig(
slackBotConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackBotConfig.id}" deleted`,
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete Slack bot config - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
>
<TrashIcon />
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackBotConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
</div>
</div>
);
};
const Main = () => {
const [slackBotTokensModalIsOpen, setSlackBotTokensModalIsOpen] =
useState(false);
const { popup, setPopup } = usePopup();
const {
data: slackBotConfigs,
isLoading: isSlackBotConfigsLoading,
error: slackBotConfigsError,
refreshSlackBotConfigs,
} = useSlackBotConfigs();
const { data: slackBotTokens, refreshSlackBotTokens } = useSlackBotTokens();
if (isSlackBotConfigsLoading) {
return <ThreeDotsLoader />;
}
if (slackBotConfigsError || !slackBotConfigs || !slackBotConfigs) {
return (
<ErrorCallout
errorTitle="Error loading slack bot configs"
errorMsg={
slackBotConfigsError.info?.message ||
slackBotConfigsError.info?.detail
}
/>
);
}
return (
<div className="mb-8">
{popup}
<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>
<Text className="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>
</Text>
<Text className="mb-6">
Follow the{" "}
<a
className="text-blue-500"
href="https://docs.danswer.dev/slack_bot_setup"
target="_blank"
rel="noreferrer"
>
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>
) : (
<>
<Text className="italic mt-3">Tokens saved!</Text>
<Button
onClick={() => {
setSlackBotTokensModalIsOpen(!slackBotTokensModalIsOpen);
}}
variant="outline"
className="mt-2"
icon={slackBotTokensModalIsOpen ? FiChevronUp : FiChevronDown}
>
{slackBotTokensModalIsOpen ? "Hide" : "Edit Tokens"}
</Button>
{slackBotTokensModalIsOpen && (
<div className="mt-3">
<SlackBotTokensForm
onClose={() => {
refreshSlackBotTokens();
setSlackBotTokensModalIsOpen(false);
}}
setPopup={setPopup}
existingTokens={slackBotTokens}
/>
</div>
)}
</>
)}
{slackBotTokens && (
<>
<Title className="mb-2 mt-4">Step 2: Setup DanswerBot</Title>
<Text className="mb-3">
Configure Danswer to automatically answer questions in Slack
channels. By default, Danswer only responds in channels where a
configuration is setup unless it is explicitly tagged.
</Text>
<div className="mb-2"></div>
<Link className="flex mb-3 w-fit" href="/admin/bot/new">
<Button className="my-auto" variant="next">
New Slack Bot Configuration
</Button>
</Link>
{slackBotConfigs.length > 0 && (
<div className="mt-8">
<SlackBotConfigsTable
slackBotConfigs={slackBotConfigs}
refresh={refreshSlackBotConfigs}
setPopup={setPopup}
/>
</div>
)}
</>
)}
</div>
);
};
const Page = () => {
return (
<div className="container mx-auto">
<AdminPageTitle
icon={<SlackIcon size={32} />}
title="Slack Bot Configuration"
/>
<InstantSSRAutoRefresh />
<Main />
</div>
);
};
export default Page;

View File

@@ -0,0 +1,31 @@
"use client";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { SlackTokensForm } from "./SlackTokensForm";
export const NewSlackBotForm = ({}: {}) => {
const [formValues] = useState({
name: "",
enabled: true,
bot_token: "",
app_token: "",
});
const { popup, setPopup } = usePopup();
const router = useRouter();
return (
<div>
{popup}
<div className="p-4">
<SlackTokensForm
isUpdate={false}
initialValues={formValues}
setPopup={setPopup}
router={router}
/>
</div>
</div>
);
};

View File

@@ -0,0 +1,121 @@
"use client";
import { PageSelector } from "@/components/PageSelector";
import { SlackBot } from "@/lib/types";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";
import { FiCheck, FiEdit, FiXCircle } from "react-icons/fi";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
const NUM_IN_PAGE = 20;
function ClickableTableRow({
url,
children,
...props
}: {
url: string;
children: React.ReactNode;
[key: string]: any;
}) {
const router = useRouter();
useEffect(() => {
router.prefetch(url);
}, [router]);
const navigate = () => {
router.push(url);
};
return (
<TableRow {...props} onClick={navigate}>
{children}
</TableRow>
);
}
export function SlackBotTable({ slackBots }: { slackBots: SlackBot[] }) {
const [page, setPage] = useState(1);
// sort by id for consistent ordering
slackBots.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
const slackBotsForPage = slackBots.slice(
NUM_IN_PAGE * (page - 1),
NUM_IN_PAGE * page
);
return (
<div>
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
<TableHead>Channel Count</TableHead>
<TableHead>Enabled</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackBotsForPage.map((slackBot) => {
return (
<ClickableTableRow
url={`/admin/bots/${slackBot.id}`}
key={slackBot.id}
className="hover:bg-muted cursor-pointer"
>
<TableCell>
<div className="flex items-center">
<FiEdit className="mr-4" />
{slackBot.name}
</div>
</TableCell>
<TableCell>{slackBot.configs_count}</TableCell>
<TableCell>
{slackBot.enabled ? (
<FiCheck className="text-emerald-600" size="18" />
) : (
<FiXCircle className="text-red-600" size="18" />
)}
</TableCell>
</ClickableTableRow>
);
})}
</TableBody>
</Table>
{slackBots.length > NUM_IN_PAGE && (
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackBots.length / NUM_IN_PAGE)}
currentPage={page}
onPageChange={(newPage) => {
setPage(newPage);
window.scrollTo({
top: 0,
left: 0,
behavior: "smooth",
});
}}
/>
</div>
</div>
)}
</div>
);
}

View File

@@ -1,34 +1,52 @@
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { SlackBotTokens } from "@/lib/types";
import { SlackBot } from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field";
import { setSlackBotTokens } from "./lib";
import CardSection from "@/components/admin/CardSection";
import { Button } from "@/components/ui/button";
import { updateSlackBot, SlackBotCreationRequest } from "./new/lib";
interface SlackBotTokensFormProps {
onClose: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
existingTokens?: SlackBotTokens;
existingSlackApp?: SlackBot;
onTokensSet?: (tokens: { bot_token: string; app_token: string }) => void;
embedded?: boolean;
noForm?: boolean;
}
export const SlackBotTokensForm = ({
onClose,
setPopup,
existingTokens,
existingSlackApp,
onTokensSet,
embedded = true,
noForm = true,
}: SlackBotTokensFormProps) => {
const Wrapper = embedded ? "div" : CardSection;
const FormWrapper = noForm ? "div" : Form;
return (
<CardSection>
<Wrapper className="w-full">
<Formik
initialValues={existingTokens || { app_token: "", bot_token: "" }}
initialValues={existingSlackApp || { app_token: "", bot_token: "" }}
validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string().required()),
document_sets: Yup.array().of(Yup.number()),
bot_token: Yup.string().required(),
app_token: Yup.string().required(),
})}
onSubmit={async (values, formikHelpers) => {
if (embedded && onTokensSet) {
onTokensSet(values);
return;
}
formikHelpers.setSubmitting(true);
const response = await setSlackBotTokens(values);
const response = await updateSlackBot(
existingSlackApp?.id || 0,
values as SlackBotCreationRequest
);
formikHelpers.setSubmitting(false);
if (response.ok) {
setPopup({
@@ -46,25 +64,29 @@ export const SlackBotTokensForm = ({
}}
>
{({ isSubmitting }) => (
<Form>
<FormWrapper className="w-full">
<TextFormField
width="w-full"
name="bot_token"
label="Slack Bot Token"
type="password"
/>
<TextFormField
width="w-full"
name="app_token"
label="Slack App Token"
type="password"
/>
<div className="flex">
<Button type="submit" disabled={isSubmitting} variant="submit">
Set Tokens
</Button>
</div>
</Form>
{!embedded && (
<div className="flex w-full">
<Button type="submit" disabled={isSubmitting} variant="submit">
Set Tokens
</Button>
</div>
)}
</FormWrapper>
)}
</Formik>
</CardSection>
</Wrapper>
);
};

View File

@@ -0,0 +1,170 @@
"use client";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SlackBot } from "@/lib/types";
import { useRouter } from "next/navigation";
import { ChevronDown, ChevronRight } from "lucide-react";
import { useState, useEffect, useRef } from "react";
import { updateSlackBotField } from "@/lib/updateSlackBotField";
import { Checkbox } from "@/app/admin/settings/SettingsForm";
import { SlackTokensForm } from "./SlackTokensForm";
import { SourceIcon } from "@/components/SourceIcon";
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
import { deleteSlackBot } from "./new/lib";
import { GenericConfirmModal } from "@/components/modals/GenericConfirmModal";
import { FiTrash } from "react-icons/fi";
import { Button } from "@/components/ui/button";
export const ExistingSlackBotForm = ({
existingSlackBot,
refreshSlackBot,
}: {
existingSlackBot: SlackBot;
refreshSlackBot?: () => void;
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const [formValues, setFormValues] = useState(existingSlackBot);
const { popup, setPopup } = usePopup();
const router = useRouter();
const dropdownRef = useRef<HTMLDivElement>(null);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const handleUpdateField = async (
field: keyof SlackBot,
value: string | boolean
) => {
try {
const response = await updateSlackBotField(
existingSlackBot,
field,
value
);
if (!response.ok) {
throw new Error(await response.text());
}
setPopup({
message: `Connector ${field} updated successfully`,
type: "success",
});
} catch (error) {
setPopup({
message: `Failed to update connector ${field}`,
type: "error",
});
}
setFormValues((prev) => ({ ...prev, [field]: value }));
};
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
dropdownRef.current &&
!dropdownRef.current.contains(event.target as Node) &&
isExpanded
) {
setIsExpanded(false);
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isExpanded]);
return (
<div>
{popup}
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div className="my-auto">
<SourceIcon iconSize={36} sourceType={"slack"} />
</div>
<EditableStringFieldDisplay
value={formValues.name}
isEditable={true}
onUpdate={(value) => handleUpdateField("name", value)}
scale={2.5}
/>
</div>
<div className="flex flex-col" ref={dropdownRef}>
<div className="flex items-center gap-4">
<div className="border rounded-lg border-gray-200">
<div
className="flex items-center gap-2 cursor-pointer hover:bg-gray-100 p-2"
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? (
<ChevronDown size={20} />
) : (
<ChevronRight size={20} />
)}
<span>Update Tokens</span>
</div>
</div>
<Button
variant="destructive"
onClick={() => setShowDeleteModal(true)}
icon={FiTrash}
tooltip="Click to delete"
>
Delete
</Button>
</div>
{isExpanded && (
<div className="bg-white border rounded-lg border-gray-200 shadow-lg absolute mt-12 right-0 z-10 w-full md:w-3/4 lg:w-1/2">
<div className="p-4">
<SlackTokensForm
isUpdate={true}
initialValues={formValues}
existingSlackBotId={existingSlackBot.id}
refreshSlackBot={refreshSlackBot}
setPopup={setPopup}
router={router}
/>
</div>
</div>
)}
</div>
</div>
<div className="mt-4">
<div className="inline-block border rounded-lg border-gray-200 px-2 py-2">
<Checkbox
label="Enabled"
checked={formValues.enabled}
onChange={(e) => handleUpdateField("enabled", e.target.checked)}
/>
</div>
{showDeleteModal && (
<GenericConfirmModal
title="Delete Slack Bot"
message="Are you sure you want to delete this Slack bot? This action cannot be undone."
confirmText="Delete"
onClose={() => setShowDeleteModal(false)}
onConfirm={async () => {
try {
const response = await deleteSlackBot(existingSlackBot.id);
if (!response.ok) {
throw new Error(await response.text());
}
setPopup({
message: "Slack bot deleted successfully",
type: "success",
});
router.push("/admin/bots");
} catch (error) {
setPopup({
message: "Failed to delete Slack bot",
type: "error",
});
}
setShowDeleteModal(false);
}}
/>
)}
</div>
</div>
);
};

View File

@@ -0,0 +1,108 @@
"use client";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { createSlackBot, updateSlackBot } from "./new/lib";
import { Button } from "@/components/ui/button";
import { SourceIcon } from "@/components/SourceIcon";
export const SlackTokensForm = ({
isUpdate,
initialValues,
existingSlackBotId,
refreshSlackBot,
setPopup,
router,
}: {
isUpdate: boolean;
initialValues: any;
existingSlackBotId?: number;
refreshSlackBot?: () => void;
setPopup: (popup: { message: string; type: "error" | "success" }) => void;
router: any;
}) => (
<Formik
initialValues={initialValues}
validationSchema={Yup.object().shape({
bot_token: Yup.string().required(),
app_token: Yup.string().required(),
name: Yup.string().required(),
})}
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
let response;
if (isUpdate) {
response = await updateSlackBot(existingSlackBotId!, values);
} else {
response = await createSlackBot(values);
}
formikHelpers.setSubmitting(false);
if (response.ok) {
if (refreshSlackBot) {
refreshSlackBot();
}
const responseJson = await response.json();
const botId = isUpdate ? existingSlackBotId : responseJson.id;
setPopup({
message: isUpdate
? "Successfully updated Slack Bot!"
: "Successfully created Slack Bot!",
type: "success",
});
router.push(`/admin/bots/${botId}}`);
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: isUpdate
? `Error updating Slack Bot - ${errorMsg}`
: `Error creating Slack Bot - ${errorMsg}`,
type: "error",
});
}
}}
enableReinitialize={true}
>
{({ isSubmitting, setFieldValue, values }) => (
<Form className="w-full">
{!isUpdate && (
<div className="flex items-center gap-2 mb-4">
<div className="my-auto">
<SourceIcon iconSize={36} sourceType={"slack"} />
</div>
<TextFormField name="name" label="Slack Bot Name" type="text" />
</div>
)}
{!isUpdate && (
<div className="mb-4">
Please enter your Slack Bot Token and Slack App Token to give
Danswerbot access to your Slack!
</div>
)}
<TextFormField
name="bot_token"
label="Slack Bot Token"
type="password"
/>
<TextFormField
name="app_token"
label="Slack App Token"
type="password"
/>
<div className="flex justify-end w-full mt-4">
<Button
type="submit"
disabled={isSubmitting}
variant="submit"
size="default"
>
{isUpdate ? "Update!" : "Create!"}
</Button>
</div>
</Form>
)}
</Formik>
);

View File

@@ -0,0 +1,157 @@
"use client";
import { PageSelector } from "@/components/PageSelector";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { EditIcon, TrashIcon } from "@/components/icons/icons";
import { SlackChannelConfig } from "@/lib/types";
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table";
import Link from "next/link";
import { useState } from "react";
import { FiArrowUpRight } from "react-icons/fi";
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
const numToDisplay = 50;
export function SlackChannelConfigsTable({
slackBotId,
slackChannelConfigs,
refresh,
setPopup,
}: {
slackBotId: number;
slackChannelConfigs: SlackChannelConfig[];
refresh: () => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}) {
const [page, setPage] = useState(1);
// sort by name for consistent ordering
slackChannelConfigs.sort((a, b) => {
if (a.id < b.id) {
return -1;
} else if (a.id > b.id) {
return 1;
} else {
return 0;
}
});
return (
<div>
<div className="rounded-md border">
<Table>
<TableHeader>
<TableRow>
<TableHead>Channel</TableHead>
<TableHead>Persona</TableHead>
<TableHead>Document Sets</TableHead>
<TableHead>Delete</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{slackChannelConfigs
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((slackChannelConfig) => {
return (
<TableRow key={slackChannelConfig.id}>
<TableCell>
<div className="flex gap-x-2">
<Link
className="cursor-pointer my-auto"
href={`/admin/bots/${slackBotId}/channels/${slackChannelConfig.id}`}
>
<EditIcon />
</Link>
<div className="my-auto">
{"#" + slackChannelConfig.channel_config.channel_name}
</div>
</div>
</TableCell>
<TableCell>
{slackChannelConfig.persona &&
!isPersonaASlackBotPersona(slackChannelConfig.persona) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
className="text-blue-500 flex hover:underline"
>
<FiArrowUpRight className="my-auto mr-1" />
{slackChannelConfig.persona.name}
</Link>
) : (
"-"
)}
</TableCell>
<TableCell>
<div>
{slackChannelConfig.persona &&
slackChannelConfig.persona.document_sets.length > 0
? slackChannelConfig.persona.document_sets
.map((documentSet) => documentSet.name)
.join(", ")
: "-"}
</div>
</TableCell>
<TableCell>
<div
className="cursor-pointer hover:text-destructive"
onClick={async () => {
const response = await deleteSlackChannelConfig(
slackChannelConfig.id
);
if (response.ok) {
setPopup({
message: `Slack bot config "${slackChannelConfig.id}" deleted`,
type: "success",
});
} else {
const errorMsg = await response.text();
setPopup({
message: `Failed to delete Slack bot config - ${errorMsg}`,
type: "error",
});
}
refresh();
}}
>
<TrashIcon />
</div>
</TableCell>
</TableRow>
);
})}
{/* Empty row with message when table has no data */}
{slackChannelConfigs.length === 0 && (
<TableRow>
<TableCell
colSpan={4}
className="text-center text-muted-foreground"
>
Please add a New Slack Bot Configuration to begin chatting
with Danswer!
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(slackChannelConfigs.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
</div>
</div>
);
}

View File

@@ -3,47 +3,55 @@
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import * as Yup from "yup";
import { usePopup } from "@/components/admin/connectors/Popup";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
import {
BooleanFormField,
Label,
SelectorFormField,
SubLabel,
TextArrayField,
TextFormField,
} from "@/components/admin/connectors/Field";
import {
createSlackBotConfig,
createSlackChannelConfig,
isPersonaASlackBotPersona,
updateSlackBotConfig,
} from "./lib";
import { Separator } from "@/components/ui/separator";
updateSlackChannelConfig,
} from "../lib";
import CardSection from "@/components/admin/CardSection";
import { Button } from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { Persona } from "../assistants/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
import { useState } from "react";
import { AdvancedOptionsToggle } from "@/components/AdvancedOptionsToggle";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import CollapsibleSection from "../assistants/CollapsibleSection";
import CollapsibleSection from "@/app/admin/assistants/CollapsibleSection";
import { StandardAnswerCategoryResponse } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { StandardAnswerCategoryDropdownField } from "@/components/standardAnswers/StandardAnswerCategoryDropdown";
import {
Tabs,
TabsList,
TabsTrigger,
TabsContent,
} from "@/components/ui/fully_wrapped_tabs";
export const SlackBotCreationForm = ({
export const SlackChannelConfigCreationForm = ({
slack_bot_id,
documentSets,
personas,
standardAnswerCategoryResponse,
existingSlackBotConfig,
existingSlackChannelConfig,
}: {
slack_bot_id: number;
documentSets: DocumentSet[];
personas: Persona[];
standardAnswerCategoryResponse: StandardAnswerCategoryResponse;
existingSlackBotConfig?: SlackBotConfig;
existingSlackChannelConfig?: SlackChannelConfig;
}) => {
const isUpdate = existingSlackBotConfig !== undefined;
const isUpdate = existingSlackChannelConfig !== undefined;
const { popup, setPopup } = usePopup();
const router = useRouter();
const existingSlackBotUsesPersona = existingSlackBotConfig?.persona
? !isPersonaASlackBotPersona(existingSlackBotConfig.persona)
const existingSlackBotUsesPersona = existingSlackChannelConfig?.persona
? !isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
: false;
const [usingPersonas, setUsingPersonas] = useState(
existingSlackBotUsesPersona
@@ -58,48 +66,52 @@ export const SlackBotCreationForm = ({
{popup}
<Formik
initialValues={{
channel_names: existingSlackBotConfig
? existingSlackBotConfig.channel_config.channel_names
: ([""] as string[]),
slack_bot_id: slack_bot_id,
channel_name:
existingSlackChannelConfig?.channel_config.channel_name,
answer_validity_check_enabled: (
existingSlackBotConfig?.channel_config?.answer_filters || []
existingSlackChannelConfig?.channel_config?.answer_filters || []
).includes("well_answered_postfilter"),
questionmark_prefilter_enabled: (
existingSlackBotConfig?.channel_config?.answer_filters || []
existingSlackChannelConfig?.channel_config?.answer_filters || []
).includes("questionmark_prefilter"),
respond_tag_only:
existingSlackBotConfig?.channel_config?.respond_tag_only || false,
existingSlackChannelConfig?.channel_config?.respond_tag_only ||
false,
respond_to_bots:
existingSlackBotConfig?.channel_config?.respond_to_bots || false,
existingSlackChannelConfig?.channel_config?.respond_to_bots ||
false,
enable_auto_filters:
existingSlackBotConfig?.enable_auto_filters || false,
existingSlackChannelConfig?.enable_auto_filters || false,
respond_member_group_list:
existingSlackBotConfig?.channel_config
existingSlackChannelConfig?.channel_config
?.respond_member_group_list ?? [],
still_need_help_enabled:
existingSlackBotConfig?.channel_config?.follow_up_tags !==
existingSlackChannelConfig?.channel_config?.follow_up_tags !==
undefined,
follow_up_tags:
existingSlackBotConfig?.channel_config?.follow_up_tags,
existingSlackChannelConfig?.channel_config?.follow_up_tags,
document_sets:
existingSlackBotConfig && existingSlackBotConfig.persona
? existingSlackBotConfig.persona.document_sets.map(
existingSlackChannelConfig && existingSlackChannelConfig.persona
? existingSlackChannelConfig.persona.document_sets.map(
(documentSet) => documentSet.id
)
: ([] as number[]),
// prettier-ignore
persona_id:
existingSlackBotConfig?.persona &&
!isPersonaASlackBotPersona(existingSlackBotConfig.persona)
? existingSlackBotConfig.persona.id
existingSlackChannelConfig?.persona &&
!isPersonaASlackBotPersona(existingSlackChannelConfig.persona)
? existingSlackChannelConfig.persona.id
: knowledgePersona?.id ?? null,
response_type: existingSlackBotConfig?.response_type || "citations",
standard_answer_categories: existingSlackBotConfig
? existingSlackBotConfig.standard_answer_categories
response_type:
existingSlackChannelConfig?.response_type || "citations",
standard_answer_categories: existingSlackChannelConfig
? existingSlackChannelConfig.standard_answer_categories
: [],
}}
validationSchema={Yup.object().shape({
channel_names: Yup.array().of(Yup.string()),
slack_bot_id: Yup.number().required(),
channel_name: Yup.string(),
response_type: Yup.string()
.oneOf(["quotes", "citations"])
.required(),
@@ -118,12 +130,10 @@ export const SlackBotCreationForm = ({
onSubmit={async (values, formikHelpers) => {
formikHelpers.setSubmitting(true);
// remove empty channel names
const cleanedValues = {
...values,
channel_names: values.channel_names.filter(
(channelName) => channelName !== ""
),
slack_bot_id: slack_bot_id,
channel_name: values.channel_name!,
respond_member_group_list: values.respond_member_group_list,
usePersona: usingPersonas,
standard_answer_categories: values.standard_answer_categories.map(
@@ -139,16 +149,16 @@ export const SlackBotCreationForm = ({
}
let response;
if (isUpdate) {
response = await updateSlackBotConfig(
existingSlackBotConfig.id,
response = await updateSlackChannelConfig(
existingSlackChannelConfig.id,
cleanedValues
);
} else {
response = await createSlackBotConfig(cleanedValues);
response = await createSlackChannelConfig(cleanedValues);
}
formikHelpers.setSubmitting(false);
if (response.ok) {
router.push(`/admin/bot?u=${Date.now()}`);
router.push(`/admin/bots/${slack_bot_id}`);
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
@@ -164,53 +174,38 @@ export const SlackBotCreationForm = ({
{({ isSubmitting, values, setFieldValue }) => (
<Form>
<div className="px-6 pb-6 pt-4 w-full">
<TextArrayField
name="channel_names"
label="Channel Names"
values={values}
subtext="The names of the Slack channels you want this configuration to apply to.
For example, #ask-danswer."
minFields={1}
placeholder="Enter channel name..."
<TextFormField
name="channel_name"
label="Slack Channel Name:"
/>
<div className="mt-6">
<Label>Knowledge Sources</Label>
<SubLabel>
Controls which information DanswerBot will pull from when
answering questions.
</SubLabel>
<div className="flex mt-4">
<button
type="button"
onClick={() => setUsingPersonas(false)}
className={`p-2 font-bold text-xs mr-3 ${
!usingPersonas
? "rounded bg-background-900 text-text-100 underline"
: "hover:underline bg-background-100"
}`}
>
Document Sets
</button>
<Tabs
defaultValue="document_sets"
className="w-full mt-4"
value={usingPersonas ? "assistants" : "document_sets"}
onValueChange={(value) =>
setUsingPersonas(value === "assistants")
}
>
<TabsList>
<TabsTrigger value="document_sets">
Document Sets
</TabsTrigger>
<TabsTrigger value="assistants">Assistants</TabsTrigger>
</TabsList>
<button
type="button"
onClick={() => setUsingPersonas(true)}
className={`p-2 font-bold text-xs ${
usingPersonas
? "rounded bg-background-900 text-text-100 underline"
: "hover:underline bg-background-100"
}`}
>
Assistants
</button>
</div>
<div className="mt-4">
{/* TODO: make this look nicer */}
{usingPersonas ? (
<TabsContent value="assistants">
<SubLabel>
Select the assistant DanswerBot will use while answering
questions in Slack.
</SubLabel>
<SelectorFormField
name="persona_id"
options={personas.map((persona) => {
@@ -220,7 +215,17 @@ export const SlackBotCreationForm = ({
};
})}
/>
) : (
</TabsContent>
<TabsContent value="document_sets">
<SubLabel>
Select the document sets DanswerBot will use while
answering questions in Slack.
</SubLabel>
<SubLabel>
Note: If No Document Sets are selected, DanswerBot will
search through all connected documents.
</SubLabel>
<FieldArray
name="document_sets"
render={(arrayHelpers: ArrayHelpers) => (
@@ -248,21 +253,14 @@ export const SlackBotCreationForm = ({
);
})}
</div>
<div>
<SubLabel>
Note: If left blank, DanswerBot will search
through all connected documents.
</SubLabel>
</div>
<div></div>
</div>
)}
/>
)}
</div>
</TabsContent>
</Tabs>
</div>
<Separator />
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}

View File

@@ -1,10 +1,9 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { CPUIcon } from "@/components/icons/icons";
import { SlackBotCreationForm } from "../SlackBotConfigCreationForm";
import { SourceIcon } from "@/components/SourceIcon";
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet, SlackBotConfig } from "@/lib/types";
import Text from "@/components/ui/text";
import { DocumentSet, SlackChannelConfig } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import {
@@ -13,16 +12,18 @@ import {
} from "@/lib/assistants/fetchAssistantsSS";
import { getStandardAnswerCategoriesIfEE } from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
async function Page(props: { params: Promise<{ id: string }> }) {
async function EditslackChannelConfigPage(props: {
params: Promise<{ id: number }>;
}) {
const params = await props.params;
const tasks = [
fetchSS("/manage/admin/slack-bot/config"),
fetchSS("/manage/admin/slack-app/channel"),
fetchSS("/manage/document-set"),
fetchAssistantsSS(),
];
const [
slackBotsResponse,
slackChannelsResponse,
documentSetsResponse,
[assistants, assistantsFetchError],
] = (await Promise.all(tasks)) as [
@@ -34,24 +35,26 @@ async function Page(props: { params: Promise<{ id: string }> }) {
const eeStandardAnswerCategoryResponse =
await getStandardAnswerCategoriesIfEE();
if (!slackBotsResponse.ok) {
if (!slackChannelsResponse.ok) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch slack bots - ${await slackBotsResponse.text()}`}
errorMsg={`Failed to fetch Slack Channels - ${await slackChannelsResponse.text()}`}
/>
);
}
const allSlackBotConfigs =
(await slackBotsResponse.json()) as SlackBotConfig[];
const slackBotConfig = allSlackBotConfigs.find(
(config) => config.id.toString() === params.id
const allslackChannelConfigs =
(await slackChannelsResponse.json()) as SlackChannelConfig[];
const slackChannelConfig = allslackChannelConfigs.find(
(config) => config.id === Number(params.id)
);
if (!slackBotConfig) {
if (!slackChannelConfig) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Did not find Slack Bot config with ID: ${params.id}`}
errorMsg={`Did not find Slack Channel config with ID: ${params.id}`}
/>
);
}
@@ -81,23 +84,19 @@ async function Page(props: { params: Promise<{ id: string }> }) {
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="Edit Slack Bot Config"
icon={<SourceIcon sourceType={"slack"} iconSize={32} />}
title="Edit Slack Channel Config"
/>
<Text className="mb-8">
Edit the existing configuration below! This config will determine how
DanswerBot behaves in the specified channels.
</Text>
<SlackBotCreationForm
<SlackChannelConfigCreationForm
slack_bot_id={slackChannelConfig.slack_bot_id}
documentSets={documentSets}
personas={assistants}
standardAnswerCategoryResponse={eeStandardAnswerCategoryResponse}
existingSlackBotConfig={slackBotConfig}
existingSlackChannelConfig={slackChannelConfig}
/>
</div>
);
}
export default Page;
export default EditslackChannelConfigPage;

View File

@@ -0,0 +1,76 @@
import { AdminPageTitle } from "@/components/admin/Title";
import { SlackChannelConfigCreationForm } from "../SlackChannelConfigCreationForm";
import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { DocumentSet } from "@/lib/types";
import { BackButton } from "@/components/BackButton";
import { fetchAssistantsSS } from "@/lib/assistants/fetchAssistantsSS";
import {
getStandardAnswerCategoriesIfEE,
StandardAnswerCategoryResponse,
} from "@/components/standardAnswers/getStandardAnswerCategoriesIfEE";
import { redirect } from "next/navigation";
import { Persona } from "../../../../assistants/interfaces";
import { SourceIcon } from "@/components/SourceIcon";
async function NewChannelConfigPage(props: {
params: Promise<{ "bot-id": string }>;
}) {
const unwrappedParams = await props.params;
const slack_bot_id_raw = unwrappedParams?.["bot-id"] || null;
const slack_bot_id = slack_bot_id_raw
? parseInt(slack_bot_id_raw as string, 10)
: null;
if (!slack_bot_id || isNaN(slack_bot_id)) {
redirect("/admin/bots");
return null;
}
const [
documentSetsResponse,
assistantsResponse,
standardAnswerCategoryResponse,
] = await Promise.all([
fetchSS("/manage/document-set") as Promise<Response>,
fetchAssistantsSS() as Promise<[Persona[], string | null]>,
getStandardAnswerCategoriesIfEE() as Promise<StandardAnswerCategoryResponse>,
]);
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[];
if (assistantsResponse[1]) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch assistants - ${assistantsResponse[1]}`}
/>
);
}
return (
<div className="container mx-auto">
<BackButton />
<AdminPageTitle
icon={<SourceIcon iconSize={32} sourceType={"slack"} />}
title="Configure DanswerBot for Slack Channel"
/>
<SlackChannelConfigCreationForm
slack_bot_id={slack_bot_id}
documentSets={documentSets}
personas={assistantsResponse[0]}
standardAnswerCategoryResponse={standardAnswerCategoryResponse}
/>
</div>
);
}
export default NewChannelConfigPage;

View File

@@ -0,0 +1,43 @@
import { errorHandlingFetcher } from "@/lib/fetcher";
import { SlackBot, SlackChannelConfig } from "@/lib/types";
import useSWR, { mutate } from "swr";
export const useSlackChannelConfigs = () => {
const url = "/api/manage/admin/slack-app/channel";
const swrResponse = useSWR<SlackChannelConfig[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackChannelConfigs: () => mutate(url),
};
};
export const useSlackBots = () => {
const url = "/api/manage/admin/slack-app/bots";
const swrResponse = useSWR<SlackBot[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBots: () => mutate(url),
};
};
export const useSlackBot = (botId: number) => {
const url = `/api/manage/admin/slack-app/bots/${botId}`;
const swrResponse = useSWR<SlackBot>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackBot: () => mutate(url),
};
};
export const useSlackChannelConfigsByBot = (botId: number) => {
const url = `/api/manage/admin/slack-app/bots/${botId}/config`;
const swrResponse = useSWR<SlackChannelConfig[]>(url, errorHandlingFetcher);
return {
...swrResponse,
refreshSlackChannelConfigs: () => mutate(url),
};
};

View File

@@ -3,13 +3,14 @@ import {
SlackBotResponseType,
SlackBotTokens,
} from "@/lib/types";
import { Persona } from "../assistants/interfaces";
import { Persona } from "@/app/admin/assistants/interfaces";
interface SlackBotConfigCreationRequest {
interface SlackChannelConfigCreationRequest {
slack_bot_id: number;
document_sets: number[];
persona_id: number | null;
enable_auto_filters: boolean;
channel_names: string[];
channel_name: string;
answer_validity_check_enabled: boolean;
questionmark_prefilter_enabled: boolean;
respond_tag_only: boolean;
@@ -22,7 +23,7 @@ interface SlackBotConfigCreationRequest {
}
const buildFiltersFromCreationRequest = (
creationRequest: SlackBotConfigCreationRequest
creationRequest: SlackChannelConfigCreationRequest
): string[] => {
const answerFilters = [] as string[];
if (creationRequest.answer_validity_check_enabled) {
@@ -35,10 +36,11 @@ const buildFiltersFromCreationRequest = (
};
const buildRequestBodyFromCreationRequest = (
creationRequest: SlackBotConfigCreationRequest
creationRequest: SlackChannelConfigCreationRequest
) => {
return JSON.stringify({
channel_names: creationRequest.channel_names,
slack_bot_id: creationRequest.slack_bot_id,
channel_name: creationRequest.channel_name,
respond_tag_only: creationRequest.respond_tag_only,
respond_to_bots: creationRequest.respond_to_bots,
enable_auto_filters: creationRequest.enable_auto_filters,
@@ -53,10 +55,10 @@ const buildRequestBodyFromCreationRequest = (
});
};
export const createSlackBotConfig = async (
creationRequest: SlackBotConfigCreationRequest
export const createSlackChannelConfig = async (
creationRequest: SlackChannelConfigCreationRequest
) => {
return fetch("/api/manage/admin/slack-bot/config", {
return fetch("/api/manage/admin/slack-app/channel", {
method: "POST",
headers: {
"Content-Type": "application/json",
@@ -65,11 +67,11 @@ export const createSlackBotConfig = async (
});
};
export const updateSlackBotConfig = async (
export const updateSlackChannelConfig = async (
id: number,
creationRequest: SlackBotConfigCreationRequest
creationRequest: SlackChannelConfigCreationRequest
) => {
return fetch(`/api/manage/admin/slack-bot/config/${id}`, {
return fetch(`/api/manage/admin/slack-app/channel/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
@@ -78,8 +80,8 @@ export const updateSlackBotConfig = async (
});
};
export const deleteSlackBotConfig = async (id: number) => {
return fetch(`/api/manage/admin/slack-bot/config/${id}`, {
export const deleteSlackChannelConfig = async (id: number) => {
return fetch(`/api/manage/admin/slack-app/channel/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
@@ -87,16 +89,6 @@ export const deleteSlackBotConfig = async (id: number) => {
});
};
export const setSlackBotTokens = async (slackBotTokens: SlackBotTokens) => {
return fetch(`/api/manage/admin/slack-bot/tokens`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(slackBotTokens),
});
};
export function isPersonaASlackBotPersona(persona: Persona) {
return persona.name.startsWith("__slack_bot_persona__");
}

View File

@@ -0,0 +1,118 @@
"use client";
import { use } from "react";
import { BackButton } from "@/components/BackButton";
import { ErrorCallout } from "@/components/ErrorCallout";
import { ThreeDotsLoader } from "@/components/Loading";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { usePopup } from "@/components/admin/connectors/Popup";
import Link from "next/link";
import { SlackChannelConfigsTable } from "./SlackChannelConfigsTable";
import { useSlackBot, useSlackChannelConfigsByBot } from "./hooks";
import { ExistingSlackBotForm } from "../SlackBotUpdateForm";
import { FiPlusSquare } from "react-icons/fi";
import { Separator } from "@/components/ui/separator";
function SlackBotEditPage({
params,
}: {
params: Promise<{ "bot-id": string }>;
}) {
// Unwrap the params promise
const unwrappedParams = use(params);
const { popup, setPopup } = usePopup();
console.log("unwrappedParams", unwrappedParams);
const {
data: slackBot,
isLoading: isSlackBotLoading,
error: slackBotError,
refreshSlackBot,
} = useSlackBot(Number(unwrappedParams["bot-id"]));
const {
data: slackChannelConfigs,
isLoading: isSlackChannelConfigsLoading,
error: slackChannelConfigsError,
refreshSlackChannelConfigs,
} = useSlackChannelConfigsByBot(Number(unwrappedParams["bot-id"]));
if (isSlackBotLoading || isSlackChannelConfigsLoading) {
return <ThreeDotsLoader />;
}
if (slackBotError || !slackBot) {
const errorMsg =
slackBotError?.info?.message ||
slackBotError?.info?.detail ||
"An unknown error occurred";
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch Slack Bot ${unwrappedParams["bot-id"]}: ${errorMsg}`}
/>
);
}
if (slackChannelConfigsError || !slackChannelConfigs) {
const errorMsg =
slackChannelConfigsError?.info?.message ||
slackChannelConfigsError?.info?.detail ||
"An unknown error occurred";
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch Slack Bot ${unwrappedParams["bot-id"]}: ${errorMsg}`}
/>
);
}
return (
<div className="container mx-auto">
<InstantSSRAutoRefresh />
<BackButton routerOverride="/admin/bots" />
<ExistingSlackBotForm
existingSlackBot={slackBot}
refreshSlackBot={refreshSlackBot}
/>
<Separator />
<div className="my-8" />
<Link
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-80
"
href={`/admin/bots/new?slack_bot_id=${unwrappedParams["bot-id"]}`}
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Slack Channel Configuration
</div>
</Link>
<div className="mt-8">
<SlackChannelConfigsTable
slackBotId={slackBot.id}
slackChannelConfigs={slackChannelConfigs}
refresh={refreshSlackChannelConfigs}
setPopup={setPopup}
/>
</div>
</div>
);
}
export default SlackBotEditPage;

View File

@@ -0,0 +1,52 @@
export interface SlackBotCreationRequest {
name: string;
enabled: boolean;
bot_token: string;
app_token: string;
}
const buildRequestBodyFromCreationRequest = (
creationRequest: SlackBotCreationRequest
) => {
return JSON.stringify({
name: creationRequest.name,
enabled: creationRequest.enabled,
bot_token: creationRequest.bot_token,
app_token: creationRequest.app_token,
});
};
export const createSlackBot = async (
creationRequest: SlackBotCreationRequest
) => {
return fetch("/api/manage/admin/slack-app/bots", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: buildRequestBodyFromCreationRequest(creationRequest),
});
};
export const updateSlackBot = async (
id: number,
creationRequest: SlackBotCreationRequest
) => {
return fetch(`/api/manage/admin/slack-app/bots/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: buildRequestBodyFromCreationRequest(creationRequest),
});
};
export const deleteSlackBot = async (id: number) => {
return fetch(`/api/manage/admin/slack-app/bots/${id}`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
};

View File

@@ -0,0 +1,14 @@
import { BackButton } from "@/components/BackButton";
import { NewSlackBotForm } from "../SlackBotCreationForm";
async function NewSlackBotPage() {
return (
<div className="container mx-auto">
<BackButton routerOverride="/admin/bots" />
<NewSlackBotForm />
</div>
);
}
export default NewSlackBotPage;

View File

@@ -0,0 +1,116 @@
"use client";
import { ErrorCallout } from "@/components/ErrorCallout";
import { FiPlusSquare } from "react-icons/fi";
import { ThreeDotsLoader } from "@/components/Loading";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import { SourceIcon } from "@/components/SourceIcon";
import { SlackBotTable } from "./SlackBotTable";
import { useSlackBots } from "./[bot-id]/hooks";
const Main = () => {
const {
data: slackBots,
isLoading: isSlackBotsLoading,
error: slackBotsError,
} = useSlackBots();
if (isSlackBotsLoading) {
return <ThreeDotsLoader />;
}
if (slackBotsError || !slackBots) {
const errorMsg =
slackBotsError?.info?.message ||
slackBotsError?.info?.detail ||
"An unknown error occurred";
return (
<ErrorCallout errorTitle="Error loading apps" errorMsg={`${errorMsg}`} />
);
}
return (
<div className="mb-8">
{/* {popup} */}
<p className="mb-2 text-sm text-muted-foreground">
Setup Slack bots that connect to Danswer. Once setup, you will be able
to ask questions to Danswer directly from Slack. Additionally, you can:
</p>
<div className="mb-2">
<ul className="list-disc mt-2 ml-4 text-sm text-muted-foreground">
<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>
<p className="mb-6 text-sm text-muted-foreground">
Follow the{" "}
<a
className="text-blue-500 hover:underline"
href="https://docs.danswer.dev/slack_bot_setup"
target="_blank"
rel="noopener noreferrer"
>
guide{" "}
</a>
found in the Danswer documentation to get started!
</p>
<Link
className="
flex
py-2
px-4
mt-2
border
border-border
h-fit
cursor-pointer
hover:bg-hover
text-sm
w-40
"
href="/admin/bots/new"
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />
New Slack Bot
</div>
</Link>
<SlackBotTable slackBots={slackBots} />
</div>
);
};
const Page = () => {
return (
<div className="container mx-auto">
<AdminPageTitle
icon={<SourceIcon iconSize={36} sourceType={"slack"} />}
title="Slack Bots"
/>
<InstantSSRAutoRefresh />
<Main />
</div>
);
};
export default Page;

View File

@@ -7,16 +7,14 @@ import { SourceIcon } from "@/components/SourceIcon";
import { CCPairStatus } from "@/components/Status";
import { usePopup } from "@/components/admin/connectors/Popup";
import CredentialSection from "@/components/credentials/CredentialSection";
import { CheckmarkIcon, EditIcon, XIcon } from "@/components/icons/icons";
import { updateConnectorCredentialPairName } from "@/lib/connector";
import { credentialTemplates } from "@/lib/connectors/credentials";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { ValidSources } from "@/lib/types";
import { Button } from "@/components/ui/button";
import Title from "@/components/ui/title";
import { Separator } from "@/components/ui/separator";
import { useRouter } from "next/navigation";
import { useCallback, useEffect, useRef, useState, use } from "react";
import { useCallback, useEffect, useState, use } from "react";
import useSWR, { mutate } from "swr";
import { AdvancedConfigDisplay, ConfigDisplay } from "./ConfigDisplay";
import { DeletionButton } from "./DeletionButton";
@@ -26,6 +24,7 @@ import { ModifyStatusButtonCluster } from "./ModifyStatusButtonCluster";
import { ReIndexButton } from "./ReIndexButton";
import { buildCCPairInfoUrl } from "./lib";
import { CCPairFullInfo, ConnectorCredentialPairStatus } from "./types";
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
// since the uploaded files are cleaned up after some period of time
// re-indexing will not work for the file connector. Also, it would not
@@ -45,22 +44,12 @@ function Main({ ccPairId }: { ccPairId: number }) {
);
const [hasLoadedOnce, setHasLoadedOnce] = useState(false);
const [editableName, setEditableName] = useState(ccPair?.name || "");
const [isEditing, setIsEditing] = useState(false);
const inputRef = useRef<HTMLInputElement>(null);
const { popup, setPopup } = usePopup();
const finishConnectorDeletion = useCallback(() => {
router.push("/admin/indexing/status?message=connector-deleted");
}, [router]);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
useEffect(() => {
if (isLoading) {
return;
@@ -78,21 +67,16 @@ function Main({ ccPairId }: { ccPairId: number }) {
}
}, [isLoading, ccPair, error, hasLoadedOnce, finishConnectorDeletion]);
const handleNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableName(e.target.value);
};
const handleUpdateName = async () => {
const handleUpdateName = async (newName: string) => {
try {
const response = await updateConnectorCredentialPairName(
ccPair?.id!,
editableName
newName
);
if (!response.ok) {
throw new Error(await response.text());
}
mutate(buildCCPairInfoUrl(ccPairId));
setIsEditing(false);
setPopup({
message: "Connector name updated successfully",
type: "success",
@@ -124,16 +108,6 @@ function Main({ ccPairId }: { ccPairId: number }) {
mutate(buildCCPairInfoUrl(ccPairId));
};
const startEditing = () => {
setEditableName(ccPair.name);
setIsEditing(true);
};
const resetEditing = () => {
setIsEditing(false);
setEditableName(ccPair.name);
};
const {
prune_freq: pruneFreq,
refresh_freq: refreshFreq,
@@ -150,37 +124,11 @@ function Main({ ccPairId }: { ccPairId: number }) {
<SourceIcon iconSize={24} sourceType={ccPair.connector.source} />
</div>
{ccPair.is_editable_for_current_user && isEditing ? (
<div className="flex items-center">
<input
ref={inputRef}
type="text"
value={editableName}
onChange={handleNameChange}
className="text-3xl w-full ring ring-1 ring-neutral-800 text-emphasis font-bold"
/>
<Button onClick={handleUpdateName}>
<CheckmarkIcon className="text-neutral-200" />
</Button>
<Button onClick={() => resetEditing()}>
<XIcon className="text-neutral-200" />
</Button>
</div>
) : (
<h1
onClick={() =>
ccPair.is_editable_for_current_user && startEditing()
}
className={`group flex ${
ccPair.is_editable_for_current_user ? "cursor-pointer" : ""
} text-3xl text-emphasis gap-x-2 items-center font-bold`}
>
{ccPair.name}
{ccPair.is_editable_for_current_user && (
<EditIcon className="group-hover:visible invisible" />
)}
</h1>
)}
<EditableStringFieldDisplay
value={ccPair.name}
isEditable={ccPair.is_editable_for_current_user}
onUpdate={handleUpdateName}
/>
{ccPair.is_editable_for_current_user && (
<div className="ml-auto flex gap-x-2">

View File

@@ -313,10 +313,8 @@ const Main = () => {
{popup}
<Text className="mb-3">
<b>Document Sets</b> allow you to group logically connected documents
into a single bundle. These can then be used as filter when performing
searches in the web UI or attached to slack bots to limit the amount of
information the bot searches over when answering in a specific channel
or with a certain command.
into a single bundle. These can then be used as a filter when performing
searches to control the scope of information Danswer searches over.
</Text>
<div className="mb-3"></div>

View File

@@ -6,7 +6,6 @@ import { Textarea } from "@/components/ui/textarea";
import { Button } from "@/components/ui/button";
import { useInputPrompt } from "../hooks";
import { EditPromptModalProps } from "../interfaces";
import { Input } from "@/components/ui/input";
const EditPromptSchema = Yup.object().shape({
prompt: Yup.string().required("Title is required"),
@@ -75,7 +74,7 @@ const EditPromptModal = ({
Title
</label>
<Field
as={Input}
as={Textarea}
id="prompt"
name="prompt"
placeholder="Title (e.g. 'Draft email')"

View File

@@ -11,19 +11,19 @@ import React, { useContext, useState, useEffect } from "react";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
function Checkbox({
export function Checkbox({
label,
sublabel,
checked,
onChange,
}: {
label: string;
sublabel: string;
sublabel?: string;
checked: boolean;
onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}) {
return (
<label className="flex text-sm mb-4">
<label className="flex text-sm">
<input
checked={checked}
onChange={onChange}
@@ -32,7 +32,7 @@ function Checkbox({
/>
<div>
<Label>{label}</Label>
<SubLabel>{sublabel}</SubLabel>
{sublabel && <SubLabel>{sublabel}</SubLabel>}
</div>
</label>
);

View File

@@ -65,7 +65,7 @@ export function DanswerBotChart({
return (
<CardSection className="mt-8">
<Title>Slack Bot</Title>
<Title>Slack Channel</Title>
<Text>Total Queries vs Auto Resolved</Text>
{chart}
</CardSection>

View File

@@ -324,8 +324,8 @@ const StandardAnswersTable = ({
<div className="mt-4">
<Text>
Ensure that you have added the category to the relevant{" "}
<a className="text-link" href="/admin/bot">
Slack bot
<a className="text-link" href="/admin/bots">
Slack Bot
</a>
.
</Text>

View File

@@ -6,8 +6,10 @@ import { FiChevronLeft } from "react-icons/fi";
export function BackButton({
behaviorOverride,
routerOverride,
}: {
behaviorOverride?: () => void;
routerOverride?: string;
}) {
const router = useRouter();
@@ -27,6 +29,8 @@ export function BackButton({
onClick={() => {
if (behaviorOverride) {
behaviorOverride();
} else if (routerOverride) {
router.push(routerOverride);
} else {
router.back();
}

View File

@@ -0,0 +1,130 @@
import { usePopup } from "@/components/admin/connectors/Popup";
import { CheckmarkIcon, EditIcon, XIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";
import { Input } from "@/components/ui/input";
import { cn } from "@/lib/utils";
interface EditableStringFieldDisplayProps {
value: string;
isEditable: boolean;
onUpdate: (newValue: string) => Promise<void>;
textClassName?: string;
scale?: number;
}
export function EditableStringFieldDisplay({
value,
isEditable,
onUpdate,
textClassName,
scale = 1,
}: EditableStringFieldDisplayProps) {
const [isEditing, setIsEditing] = useState(false);
const [editableValue, setEditableValue] = useState(value);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const { popup, setPopup } = usePopup();
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node) &&
isEditing
) {
resetEditing();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isEditing]);
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableValue(e.target.value);
};
const handleUpdate = async () => {
await onUpdate(editableValue);
setIsEditing(false);
};
const resetEditing = () => {
setIsEditing(false);
setEditableValue(value);
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (e.key === "Enter") {
handleUpdate();
}
};
return (
<div ref={containerRef} className={"flex items-center"}>
{popup}
<Input
ref={inputRef as React.RefObject<HTMLInputElement>}
type="text"
value={editableValue}
onChange={handleValueChange}
onKeyDown={handleKeyDown}
className={cn(textClassName, isEditing ? "block" : "hidden")}
style={{ fontSize: `${scale}rem` }}
/>
{!isEditing && (
<span
onClick={() => isEditable && setIsEditing(true)}
className={cn(textClassName, "cursor-pointer")}
style={{ fontSize: `${scale}rem` }}
>
{value}
</span>
)}
{isEditing && isEditable ? (
<>
<div className={cn("flex", "flex-row")}>
<Button
onClick={handleUpdate}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<CheckmarkIcon className={`text-600`} size={12 * scale} />
</Button>
<Button
onClick={resetEditing}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<XIcon className={`text-600`} size={12 * scale} />
</Button>
</div>
</>
) : (
<h1
onClick={() => isEditable && setIsEditing(true)}
className={`group flex ${isEditable ? "cursor-pointer" : ""} ${""}`}
style={{ fontSize: `${scale}rem` }}
>
{isEditable && (
<EditIcon className={`visible ml-2`} size={8 * scale} />
)}
</h1>
)}
</div>
);
}

View File

@@ -0,0 +1,111 @@
import { usePopup } from "@/components/admin/connectors/Popup";
import { CheckmarkIcon, XIcon } from "@/components/icons/icons";
import { Button } from "@/components/ui/button";
import { useEffect, useRef, useState } from "react";
import { Textarea } from "@/components/ui/textarea";
import { cn } from "@/lib/utils";
interface EditableTextAreaDisplayProps {
value: string;
isEditable: boolean;
onUpdate: (newValue: string) => Promise<void>;
textClassName?: string;
scale?: number;
}
export function EditableTextAreaDisplay({
value,
isEditable,
onUpdate,
textClassName,
scale = 1,
}: EditableTextAreaDisplayProps) {
const [isEditing, setIsEditing] = useState(false);
const [editableValue, setEditableValue] = useState(value);
const inputRef = useRef<HTMLInputElement | HTMLTextAreaElement>(null);
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isEditing && inputRef.current) {
inputRef.current.focus();
}
}, [isEditing]);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (
containerRef.current &&
!containerRef.current.contains(event.target as Node) &&
isEditing
) {
resetEditing();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [isEditing]);
const handleValueChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setEditableValue(e.target.value);
};
const handleUpdate = async () => {
await onUpdate(editableValue);
setIsEditing(false);
};
const resetEditing = () => {
setIsEditing(false);
setEditableValue(value);
};
const handleKeyDown = (
e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>
) => {
if (e.key === "Enter") {
handleUpdate();
}
};
return (
<div ref={containerRef} className={"flex items-center"}>
<Textarea
ref={inputRef as React.RefObject<HTMLTextAreaElement>}
value={editableValue}
onChange={(e) => setEditableValue(e.target.value)}
onKeyDown={handleKeyDown}
className={cn(
textClassName,
"bg-white",
isEditable && !isEditing && "cursor-pointer"
)}
style={{ fontSize: `${scale}rem` }}
readOnly={!isEditing}
onClick={() => isEditable && !isEditing && setIsEditing(true)}
/>
{isEditing && isEditable ? (
<div className={cn("flex", "flex-col gap-1")}>
<Button
onClick={handleUpdate}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<CheckmarkIcon className={`text-600`} size={12 * scale} />
</Button>
<Button
onClick={resetEditing}
variant="ghost"
size="sm"
className="p-0 hover:bg-transparent ml-2"
>
<XIcon className={`text-600`} size={12 * scale} />
</Button>
</div>
) : null}
</div>
);
}

View File

@@ -155,7 +155,7 @@ export function ClientLayout({
<div className="ml-1">Slack Bots</div>
</div>
),
link: "/admin/bot",
link: "/admin/bots",
},
{
name: (

View File

@@ -2,7 +2,6 @@
import React from "react";
import Text from "@/components/ui/text";
import { Button } from "@/components/ui/button";
import { Modal } from "../../Modal";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";

View File

@@ -3,17 +3,33 @@ import * as React from "react";
import { cn } from "@/lib/utils";
export interface InputProps
extends React.InputHTMLAttributes<HTMLInputElement> {}
extends React.InputHTMLAttributes<HTMLInputElement> {
isEditing?: boolean;
}
const Input = React.forwardRef<HTMLInputElement, InputProps>(
({ className, type, ...props }, ref) => {
({ className, type, isEditing = true, style, ...props }, ref) => {
const textClassName = "text-2xl text-strong dark:text-neutral-50";
if (!isEditing) {
return (
<span className={cn(textClassName, className)}>
{props.value || props.defaultValue}
</span>
);
}
return (
<input
type={type}
className={cn(
"flex h-10 w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-neutral-950 placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-neutral-950 dark:ring-offset-neutral-950 dark:file:text-neutral-50 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
textClassName,
"w-[1ch] min-w-[1ch] box-content pr-1",
className
)}
style={{
width: `${Math.max(1, String(props.value || props.defaultValue || "").length)}ch`,
...style,
}}
ref={ref}
{...props}
/>

View File

@@ -10,7 +10,31 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
return (
<textarea
className={cn(
"flex min-h-[80px] w-full rounded-md border border-neutral-200 bg-white px-3 py-2 text-sm ring-offset-white placeholder:text-neutral-500 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-neutral-950 focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-neutral-800 dark:bg-neutral-950 dark:ring-offset-neutral-950 dark:placeholder:text-neutral-400 dark:focus-visible:ring-neutral-300",
[
"flex",
"min-h-[80px]",
"w-full",
"rounded-md",
"border",
"border-neutral-200",
"bg-white",
"px-3",
"py-2",
"text-sm",
"ring-offset-white",
"placeholder:text-neutral-500",
// "focus-visible:outline-none",
// "focus-visible:ring-2",
// "focus-visible:ring-neutral-950",
// "focus-visible:ring-offset-2",
"disabled:cursor-not-allowed",
"disabled:opacity-50",
"dark:border-neutral-800",
"dark:bg-neutral-950",
"dark:ring-offset-neutral-950",
"dark:placeholder:text-neutral-400",
"dark:focus-visible:ring-neutral-300",
].join(" "),
className
)}
ref={ref}

View File

@@ -571,7 +571,7 @@ Hint: Use the singular form of the object name (e.g., 'Opportunity' instead of '
name: "channels",
description: `Specify 0 or more channels to index. For example, specifying the channel "support" will cause us to only index all content within the "#support" channel. If no channels are specified, all channels in your workspace will be indexed.`,
optional: true,
// Slack channels can only be lowercase
// Slack Channels can only be lowercase
transform: (values) => values.map((value) => value.toLowerCase()),
},
{

View File

@@ -204,7 +204,7 @@ export type AnswerFilterOption =
| "questionmark_prefilter";
export interface ChannelConfig {
channel_names: string[];
channel_name: string;
respond_tag_only?: boolean;
respond_to_bots?: boolean;
respond_member_group_list?: string[];
@@ -214,8 +214,9 @@ export interface ChannelConfig {
export type SlackBotResponseType = "quotes" | "citations";
export interface SlackBotConfig {
export interface SlackChannelConfig {
id: number;
slack_bot_id: number;
persona: Persona | null;
channel_config: ChannelConfig;
response_type: SlackBotResponseType;
@@ -223,6 +224,17 @@ export interface SlackBotConfig {
enable_auto_filters: boolean;
}
export interface SlackBot {
id: number;
name: string;
enabled: boolean;
configs_count: number;
// tokens
bot_token: string;
app_token: string;
}
export interface SlackBotTokens {
bot_token: string;
app_token: string;

View File

@@ -0,0 +1,18 @@
import { SlackBot } from "@/lib/types";
export async function updateSlackBotField(
slackBot: SlackBot,
field: keyof SlackBot,
value: any
): Promise<Response> {
return fetch(`/api/manage/admin/slack-app/bots/${slackBot.id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
...slackBot,
[field]: value,
}),
});
}