mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-26 03:48:49 +02:00
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:
@@ -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),
|
||||
};
|
||||
};
|
@@ -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;
|
@@ -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;
|
31
web/src/app/admin/bots/SlackBotCreationForm.tsx
Normal file
31
web/src/app/admin/bots/SlackBotCreationForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
121
web/src/app/admin/bots/SlackBotTable.tsx
Normal file
121
web/src/app/admin/bots/SlackBotTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
};
|
170
web/src/app/admin/bots/SlackBotUpdateForm.tsx
Normal file
170
web/src/app/admin/bots/SlackBotUpdateForm.tsx
Normal 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>
|
||||
);
|
||||
};
|
108
web/src/app/admin/bots/SlackTokensForm.tsx
Normal file
108
web/src/app/admin/bots/SlackTokensForm.tsx
Normal 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>
|
||||
);
|
157
web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx
Normal file
157
web/src/app/admin/bots/[bot-id]/SlackChannelConfigsTable.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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}
|
@@ -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;
|
76
web/src/app/admin/bots/[bot-id]/channels/new/page.tsx
Normal file
76
web/src/app/admin/bots/[bot-id]/channels/new/page.tsx
Normal 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;
|
43
web/src/app/admin/bots/[bot-id]/hooks.ts
Normal file
43
web/src/app/admin/bots/[bot-id]/hooks.ts
Normal 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),
|
||||
};
|
||||
};
|
@@ -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__");
|
||||
}
|
118
web/src/app/admin/bots/[bot-id]/page.tsx
Normal file
118
web/src/app/admin/bots/[bot-id]/page.tsx
Normal 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;
|
52
web/src/app/admin/bots/new/lib.ts
Normal file
52
web/src/app/admin/bots/new/lib.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
};
|
14
web/src/app/admin/bots/new/page.tsx
Normal file
14
web/src/app/admin/bots/new/page.tsx
Normal 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;
|
116
web/src/app/admin/bots/page.tsx
Normal file
116
web/src/app/admin/bots/page.tsx
Normal 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;
|
@@ -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">
|
||||
|
@@ -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>
|
||||
|
@@ -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')"
|
||||
|
@@ -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>
|
||||
);
|
||||
|
@@ -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>
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
}
|
||||
|
130
web/src/components/EditableStringFieldDisplay.tsx
Normal file
130
web/src/components/EditableStringFieldDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
111
web/src/components/EditableTextAreaDisplay.tsx
Normal file
111
web/src/components/EditableTextAreaDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -155,7 +155,7 @@ export function ClientLayout({
|
||||
<div className="ml-1">Slack Bots</div>
|
||||
</div>
|
||||
),
|
||||
link: "/admin/bot",
|
||||
link: "/admin/bots",
|
||||
},
|
||||
{
|
||||
name: (
|
||||
|
@@ -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";
|
||||
|
@@ -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}
|
||||
/>
|
||||
|
@@ -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}
|
||||
|
@@ -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()),
|
||||
},
|
||||
{
|
||||
|
@@ -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;
|
||||
|
18
web/src/lib/updateSlackBotField.ts
Normal file
18
web/src/lib/updateSlackBotField.ts
Normal 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,
|
||||
}),
|
||||
});
|
||||
}
|
Reference in New Issue
Block a user