This commit is contained in:
Weves
2023-12-05 18:17:53 -08:00
committed by Chris Weaver
parent 65fde8f1b3
commit 16c8969028
130 changed files with 5328 additions and 1888 deletions

View File

@ -9,9 +9,9 @@ class CreatePromptRequest(BaseModel):
shared: bool
system_prompt: str
task_prompt: str
include_citations: bool
datetime_aware: bool
persona_ids: list[int]
include_citations: bool = False
datetime_aware: bool = False
persona_ids: list[int] | None = None
class PromptSnapshot(BaseModel):

View File

@ -59,6 +59,7 @@ def get_user_chat_sessions(
ChatSessionDetails(
id=chat.id,
name=chat.description,
persona_id=chat.persona_id,
time_created=chat.time_created.isoformat(),
)
for chat in chat_sessions

View File

@ -96,6 +96,7 @@ class RenameChatSessionResponse(BaseModel):
class ChatSessionDetails(BaseModel):
id: int
name: str
persona_id: int
time_created: str

View File

@ -24,20 +24,35 @@ const nextConfig = {
// In production, something else (nginx in the one box setup) should take
// care of this redirect. TODO (chris): better support setups where
// web_server and api_server are on different machines.
if (process.env.NODE_ENV === "production") return [];
return [
const defaultRedirects = [
{
source: "/api/stream-direct-qa:params*",
destination: "http://127.0.0.1:8080/stream-direct-qa:params*", // Proxy to Backend
permanent: true,
},
{
source: "/api/stream-query-validation:params*",
destination: "http://127.0.0.1:8080/stream-query-validation:params*", // Proxy to Backend
source: "/",
destination: "/search",
permanent: true,
},
];
if (process.env.NODE_ENV === "production") return defaultRedirects;
return defaultRedirects.concat([
{
source: "/api/chat/send-message:params*",
destination: "http://127.0.0.1:8080/chat/send-message:params*", // Proxy to Backend
permanent: true,
},
{
source: "/api/query/stream-answer-with-quote:params*",
destination:
"http://127.0.0.1:8080/query/stream-answer-with-quote:params*", // Proxy to Backend
permanent: true,
},
{
source: "/api/query/stream-query-validation:params*",
destination:
"http://127.0.0.1:8080/query/stream-query-validation:params*", // Proxy to Backend
permanent: true,
},
]);
},
publicRuntimeConfig: {
version,

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.7 KiB

After

Width:  |  Height:  |  Size: 6.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

View File

@ -13,20 +13,20 @@ function SourceTile({ sourceMetadata }: { sourceMetadata: SourceMetadata }) {
flex-col
items-center
justify-center
bg-dark-tremor-background-muted
p-4
rounded-lg
w-40
cursor-pointer
bg-hover-light
shadow-md
hover:bg-gray-800
hover:bg-hover
`}
href={sourceMetadata.adminUrl}
>
<SourceIcon sourceType={sourceMetadata.internalName} iconSize={24} />
<span className="font-medium text-sm text-gray-300 mt-2">
<Text className="font-medium text-sm mt-2">
{sourceMetadata.displayName}
</span>
</Text>
</Link>
);
}
@ -42,26 +42,26 @@ export default function Page() {
);
return (
<div className="mx-auto container dark">
<div className="mx-auto container">
<AdminPageTitle
icon={<ConnectorIcon size={32} />}
title="Add Connector"
/>
<div className="text-gray-300 text-sm">
<Text>
Connect Danswer to your organization&apos;s knowledge sources.
We&apos;ll automatically sync your data into Danswer, so you can find
exactly what you&apos;re looking for in one place.
</div>
</Text>
<div className="flex mt-8">
<Title>Import Knowledge</Title>
</div>
<div className="text-gray-300 text-sm">
<Text>
Connect to pieces of knowledge that live outside your apps. Upload
files, scrape websites, or connect to your organization&apos;s Google
Site.
</div>
</Text>
<div className="flex flex-wrap gap-4 p-4">
{importedKnowledgeSources.map((source) => {
return (
@ -73,11 +73,11 @@ export default function Page() {
<div className="flex mt-8">
<Title>Setup Auto-Syncing from Apps</Title>
</div>
<div className="text-gray-300 text-sm">
<Text>
Setup auto-syncing from your organization&apos;s most used apps and
services. Unless otherwise specified during the connector setup, we will
pull in the latest updates from the source every 10 minutes.
</div>
</Text>
<div className="flex flex-wrap gap-4 p-4">
{appConnectionSources.map((source) => {
return (

View File

@ -17,6 +17,7 @@ import {
updateSlackBotConfig,
} from "./lib";
import {
Button,
Card,
Divider,
Tab,
@ -52,7 +53,7 @@ export const SlackBotCreationForm = ({
);
return (
<div className="dark">
<div>
<Card>
{popup}
<Formik
@ -193,7 +194,7 @@ export const SlackBotCreationForm = ({
Use either a Persona <b>or</b> Document Sets to control how
DanswerBot answers.
</Text>
<div className="text-dark-tremor-content text-sm">
<Text>
<ul className="list-disc mt-2 ml-4">
<li>
You should use a Persona if you also want to customize
@ -204,7 +205,7 @@ export const SlackBotCreationForm = ({
which documents DanswerBot uses as references.
</li>
</ul>
</div>
</Text>
<Text className="mt-2">
<b>NOTE:</b> whichever tab you are when you submit the form
will be the one that is used. For example, if you are on the
@ -249,13 +250,13 @@ export const SlackBotCreationForm = ({
py-1
rounded-lg
border
border-gray-700
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-gray-600"
: " bg-gray-900 hover:bg-gray-700")
? " bg-hover"
: " bg-background hover:bg-hover-light")
}
onClick={() => {
if (isSelected) {
@ -289,7 +290,6 @@ export const SlackBotCreationForm = ({
value: persona.id,
};
})}
includeDefault={true}
/>
</TabPanel>
</TabPanels>
@ -298,17 +298,13 @@ export const SlackBotCreationForm = ({
<Divider />
<div className="flex">
<button
<Button
type="submit"
disabled={isSubmitting}
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full max-w-sm mx-auto"
}
className="mx-auto w-64"
>
{isUpdate ? "Update!" : "Create!"}
</button>
</Button>
</div>
</div>
</Form>

View File

@ -11,7 +11,7 @@ import {
setSlackBotTokens,
updateSlackBotConfig,
} from "./lib";
import { Card } from "@tremor/react";
import { Button, Card } from "@tremor/react";
interface SlackBotTokensFormProps {
onClose: () => void;
@ -64,17 +64,9 @@ export const SlackBotTokensForm = ({
type="password"
/>
<div className="flex">
<button
type="submit"
disabled={isSubmitting}
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full max-w-sm mx-auto"
}
>
<Button type="submit" disabled={isSubmitting}>
Set Tokens
</button>
</Button>
</div>
</Form>
)}

View File

@ -62,7 +62,7 @@ async function Page({ params }: { params: { id: string } }) {
const personas = (await personasResponse.json()) as Persona[];
return (
<div className="container mx-auto dark">
<div className="container mx-auto">
<InstantSSRAutoRefresh />
<BackButton />

View File

@ -33,7 +33,7 @@ async function Page() {
const personas = (await personasResponse.json()) as Persona[];
return (
<div className="container mx-auto dark">
<div className="container mx-auto">
<BackButton />
<AdminPageTitle
icon={<CPUIcon size={32} />}

View File

@ -173,7 +173,7 @@ const Main = () => {
}
return (
<div className="mb-8 dark">
<div className="mb-8">
{popup}
<Text className="mb-2">
@ -181,7 +181,7 @@ const Main = () => {
to ask questions to Danswer directly from Slack. Additionally, you can:
</Text>
<div className="text-dark-tremor-content text-sm mb-2">
<Text className="mb-2">
<ul className="list-disc mt-2 ml-4">
<li>
Setup DanswerBot to automatically answer questions in certain
@ -196,7 +196,7 @@ const Main = () => {
UI.
</li>
</ul>
</div>
</Text>
<Text className="mb-6">
Follow the{" "}
@ -226,7 +226,7 @@ const Main = () => {
setSlackBotTokensModalIsOpen(!slackBotTokensModalIsOpen);
console.log(slackBotTokensModalIsOpen);
}}
variant="secondary"
color="blue"
size="xs"
className="mt-2"
icon={slackBotTokensModalIsOpen ? FiChevronUp : FiChevronDown}
@ -259,7 +259,7 @@ const Main = () => {
<div className="mb-2"></div>
<Link className="flex mb-3" href="/admin/bot/new">
<Button className="my-auto" variant="secondary" size="xs">
<Button className="my-auto" color="green" size="xs">
New Slack Bot Configuration
</Button>
</Link>

View File

@ -1,6 +1,6 @@
import { getNameFromPath } from "@/lib/fileUtils";
import { ValidSources } from "@/lib/types";
import { List, ListItem, Card, Title, Divider } from "@tremor/react";
import { List, ListItem, Card, Title } from "@tremor/react";
function convertObjectToString(obj: any): string | any {
// Check if obj is an object and not an array or null

View File

@ -30,7 +30,6 @@ export function DeletionButton({ ccPair }: { ccPair: CCPairFullInfo }) {
<div>
{popup}
<Button
variant="secondary"
size="xs"
color="red"
onClick={() =>

View File

@ -19,7 +19,7 @@ export function ModifyStatusButtonCluster({
{popup}
{ccPair.connector.disabled ? (
<Button
variant="secondary"
color="green"
size="xs"
onClick={() =>
disableConnector(ccPair.connector, setPopup, () => router.refresh())
@ -30,7 +30,7 @@ export function ModifyStatusButtonCluster({
</Button>
) : (
<Button
variant="secondary"
color="red"
size="xs"
onClick={() =>
disableConnector(ccPair.connector, setPopup, () => router.refresh())

View File

@ -22,7 +22,7 @@ export function ReIndexButton({
{popup}
<Button
className="ml-auto"
variant="secondary"
color="green"
size="xs"
onClick={async () => {
const errorMsg = await runConnector(connectorId, [credentialId]);

View File

@ -56,14 +56,14 @@ export default async function Page({
return (
<>
<SSRAutoRefresh />
<div className="mx-auto container dark">
<div className="mx-auto container">
<div className="mb-4">
<HealthCheckBanner />
</div>
<BackButton />
<div className="pb-1 flex mt-1">
<h1 className="text-3xl font-bold">{ccPair.name}</h1>
<h1 className="text-3xl text-emphasis font-bold">{ccPair.name}</h1>
<div className="ml-auto">
<ModifyStatusButtonCluster ccPair={ccPair} />
@ -76,9 +76,9 @@ export default async function Page({
isDeleting={isDeleting}
/>
<div className="text-gray-400 text-sm mt-1">
<div className="text-sm mt-1">
Total Documents Indexed:{" "}
<b className="text-gray-300">{totalDocsIndexed}</b>
<b className="text-emphasis">{totalDocsIndexed}</b>
</div>
<Divider />

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Text, Title } from "@tremor/react";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -69,19 +71,19 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your API details
</h2>
</Title>
{bookstackCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing API Token: </p>
<p className="ml-1 italic my-auto max-w-md">
<Text className="my-auto">Existing API Token: </Text>
<Text className="ml-1 italic my-auto max-w-md">
{bookstackCredential.credential_json?.bookstack_api_token_id}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (bookstackConnectorIndexingStatuses.length > 0) {
setPopup({
@ -101,15 +103,15 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To get started you&apos;ll need API token details for your BookStack
instance. You can get these by editing your (or another) user
account in BookStack and creating a token via the &apos;API
Tokens&apos; section at the bottom. Your user account will require
to be assigned a BookStack role which has the &apos;Access system
API&apos; system permission assigned.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
</Text>
<Card className="mt-2 mb-4">
<CredentialForm<BookstackCredentialJson>
formBody={
<>
@ -151,19 +153,19 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
{bookstackConnectorIndexingStatuses.length > 0 && (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
BookStack indexing status
</h2>
<p className="text-sm mb-2">
</Title>
<Text className="mb-2">
The latest page, chapter, book and shelf changes are fetched every
10 minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<BookstackConfig, BookstackCredentialJson>
connectorIndexingStatuses={bookstackConnectorIndexingStatuses}
@ -192,12 +194,12 @@ const Main = () => {
{bookstackCredential &&
bookstackConnectorIndexingStatuses.length === 0 && (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Create Connection</h2>
<p className="text-sm mb-4">
<Text className="mb-4">
Press connect below to start the connection to your BookStack
instance.
</p>
</Text>
<ConnectorForm<BookstackConfig>
nameBuilder={(values) => `BookStackConnector`}
ccPairNameBuilder={(values) => `BookStackConnector`}
@ -209,17 +211,17 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={bookstackCredential.id}
/>
</div>
</Card>
</>
)}
{!bookstackCredential && (
<>
<p className="text-sm mb-4">
<Text className="mb-4">
Please provide your API details in Step 1 first! Once done with
that, you&apos;ll be able to start the connection then see indexing
status.
</p>
</Text>
</>
)}
</>
@ -232,10 +234,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<BookstackIcon size={32} />
<h1 className="text-3xl font-bold pl-2">BookStack</h1>
</div>
<AdminPageTitle icon={<BookstackIcon size={32} />} title="Bookstack" />
<Main />
</div>
);

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Divider, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const extractSpaceFromCloudUrl = (wikiUrl: string): string => {
const parsedUrl = new URL(wikiUrl);
@ -100,9 +102,9 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your access token
</h2>
</Title>
{confluenceCredential ? (
<>
@ -118,7 +120,7 @@ const Main = () => {
{confluenceCredential.credential_json?.confluence_access_token}
</p>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (confluenceConnectorIndexingStatuses.length > 0) {
setPopup({
@ -138,17 +140,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the Confluence connector, first follow the guide{" "}
<a
className="text-blue-500"
className="text-link"
href="https://docs.danswer.dev/connectors/confluence#setting-up"
target="_blank"
>
here
</a>{" "}
to generate an Access Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<ConfluenceCredentialJson>
formBody={
<>
@ -178,7 +181,7 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
@ -254,10 +257,11 @@ const Main = () => {
}
/>
</div>
<Divider />
</>
)}
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Add a New Space</h2>
<ConnectorForm<ConfluenceConfig>
nameBuilder={(values) =>
@ -284,14 +288,14 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={confluenceCredential.id}
/>
</div>
</Card>
</>
) : (
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with that,
you can then specify which Confluence spaces you want to make
searchable.
</p>
</Text>
)}
</>
);
@ -303,10 +307,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<ConfluenceIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Confluence</h1>
</div>
<AdminPageTitle icon={<ConfluenceIcon size={32} />} title="Confluence" />
<Main />
</div>
);

View File

@ -21,6 +21,8 @@ import {
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { usePublicCredentials } from "@/lib/hooks";
import { Title, Text, Card, Divider } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const MainSection = () => {
const { mutate } = useSWRConfig();
@ -71,18 +73,18 @@ const MainSection = () => {
return (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide Credentials
</h2>
</Title>
{document360Credential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Document360 API Token: </p>
<p className="ml-1 italic my-auto">
<Text className="my-auto">Existing Document360 API Token: </Text>
<Text className="ml-1 italic my-auto">
{document360Credential.credential_json.document360_api_token}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
await adminDeleteCredential(document360Credential.id);
refreshCredentials();
@ -94,19 +96,20 @@ const MainSection = () => {
</>
) : (
<>
<p className="text-sm mb-4">
<Text className="mb-4">
To use the Document360 connector, you must first provide the API
token and portal ID corresponding to your Document360 setup. See
setup guide{" "}
<a
className="text-blue-500"
className="text-link"
href="https://docs.danswer.dev/connectors/document360"
target="_blank"
>
here
</a>{" "}
for more detail.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-2">
<CredentialForm<Document360CredentialJson>
formBody={
<>
@ -134,20 +137,20 @@ const MainSection = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Which categories do you want to make searchable?
</h2>
</Title>
{document360ConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We index the latest articles from each workspace listed below
regularly.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<Document360Config, Document360CredentialJson>
connectorIndexingStatuses={document360ConnectorIndexingStatuses}
@ -187,11 +190,12 @@ const MainSection = () => {
}}
/>
</div>
<Divider />
</>
)}
{document360Credential ? (
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Connect to a New Workspace</h2>
<ConnectorForm<Document360Config>
nameBuilder={(values) =>
@ -232,13 +236,13 @@ const MainSection = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={document360Credential.id}
/>
</div>
</Card>
) : (
<p className="text-sm">
<Text>
Please provide your Document360 API token and portal ID in Step 1
first! Once done with that, you can then specify which Document360
categories you want to make searchable.
</p>
</Text>
)}
</>
);
@ -250,10 +254,12 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<Document360Icon size={32} />
<h1 className="text-3xl font-bold pl-2">Document360</h1>
</div>
<AdminPageTitle
icon={<Document360Icon size={32} />}
title="Document360"
/>
<MainSection />
</div>
);

View File

@ -18,6 +18,8 @@ import { Form, Formik } from "formik";
import { TextFormField } from "@/components/admin/connectors/Field";
import { FileUpload } from "@/components/admin/connectors/FileUpload";
import { getNameFromPath } from "@/lib/fileUtils";
import { Button, Card, Divider, Text } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
@ -48,13 +50,13 @@ const Main = () => {
<div>
{popup}
{filesAreUploading && <Spinner />}
<p className="text-sm mb-2">
<Text className="mb-2">
Specify files below, click the <b>Upload</b> button, and the contents of
these files will be searchable via Danswer! Currently only <i>.txt</i>,{" "}
<i>.pdf</i> and <i>.zip</i> files (containing only <i>.txt</i> files)
are supported.
</p>
<div className="text-sm mb-3">
</Text>
<Text className="mb-3">
<b>NOTE:</b> if the original document is accessible via a link, you can
add a line at the very beginning of the file that looks like:
<div className="flex my-2">
@ -67,13 +69,14 @@ const Main = () => {
search result. More details on this can be found in the{" "}
<a
href="https://docs.danswer.dev/connectors/file"
className="text-blue-500"
className="text-link"
>
documentation.
</a>
</div>
</Text>
<div className="flex mt-4">
<div className="mx-auto max-w-3xl w-full">
<div className="mx-auto w-full">
<Card>
<Formik
initialValues={{
name: "",
@ -142,7 +145,8 @@ const Main = () => {
formikHelpers.setSubmitting(false);
return;
}
const credentialId = (await createCredentialResponse.json()).id;
const credentialId = (await createCredentialResponse.json())
.id;
const credentialResponse = await linkCredential(
connector.id,
@ -159,9 +163,10 @@ const Main = () => {
return;
}
const runConnectorErrorMsg = await runConnector(connector.id, [
0,
]);
const runConnectorErrorMsg = await runConnector(
connector.id,
[0]
);
if (runConnectorErrorMsg) {
setPopup({
message: `Unable to run connector - ${runConnectorErrorMsg}`,
@ -189,8 +194,10 @@ const Main = () => {
}}
>
{({ values, isSubmitting }) => (
<Form className="p-3 border border-gray-600 rounded">
<h2 className="font-bold text-xl mb-2">Upload Files</h2>
<Form>
<h2 className="font-bold text-emphasis text-xl mb-2">
Upload Files
</h2>
<TextFormField
name="name"
label="Name:"
@ -198,32 +205,36 @@ const Main = () => {
autoCompleteDisabled={true}
/>
<p className="mb-1">Files:</p>
<p className="mb-1 font-medium text-emphasis">Files:</p>
<FileUpload
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
/>
<button
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full mx-auto mt-4"
}
<div className="flex">
<Button
className="mt-4 w-64 mx-auto"
color="green"
size="xs"
type="submit"
disabled={
selectedFiles.length === 0 || !values.name || isSubmitting
selectedFiles.length === 0 ||
!values.name ||
isSubmitting
}
>
Upload!
</button>
</Button>
</div>
</Form>
)}
</Formik>
</Card>
</div>
</div>
{fileIndexingStatuses.length > 0 && (
<div className="mt-6">
<div>
<Divider />
<h2 className="font-bold text-xl mb-2">Indexed Files</h2>
<SingleUseConnectorsTable<FileConfig, {}>
connectorIndexingStatuses={fileIndexingStatuses}
@ -253,10 +264,9 @@ export default function File() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<FileIcon size={32} />
<h1 className="text-3xl font-bold pl-2">File</h1>
</div>
<AdminPageTitle icon={<FileIcon size={32} />} title="File" />
<Main />
</div>
);

View File

@ -18,6 +18,8 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Divider, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const { mutate } = useSWRConfig();
@ -66,9 +68,9 @@ const Main = () => {
return (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your access token
</h2>
</Title>
{githubCredential ? (
<>
{" "}
@ -78,7 +80,7 @@ const Main = () => {
{githubCredential.credential_json.github_access_token}
</p>{" "}
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
await adminDeleteCredential(githubCredential.id);
refreshCredentials();
@ -90,17 +92,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
If you don&apos;t have an access token, read the guide{" "}
<a
className="text-blue-500"
href="https://docs.danswer.dev/connectors/github"
target="_blank"
>
here
</a>{" "}
on how to get one from Github.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<GithubCredentialJson>
formBody={
<>
@ -125,20 +128,20 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Which repositories do you want to make searchable?
</h2>
</Title>
{githubConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We pull the latest Pull Requests from each repository listed below
every <b>10</b> minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<GithubConfig, GithubCredentialJson>
connectorIndexingStatuses={githubConnectorIndexingStatuses}
@ -168,11 +171,12 @@ const Main = () => {
}
/>
</div>
<Divider />
</>
)}
{githubCredential ? (
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Connect to a New Repository</h2>
<ConnectorForm<GithubConfig>
nameBuilder={(values) =>
@ -208,13 +212,13 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={githubCredential.id}
/>
</div>
</Card>
) : (
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with that,
you can then specify which Github repositories you want to make
searchable.
</p>
</Text>
)}
</>
);
@ -226,10 +230,12 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GithubIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Github PRs</h1>
</div>
<AdminPageTitle
icon={<GithubIcon size={32} />}
title="Github PRs + Issues"
/>
<Main />
</div>
);

View File

@ -22,6 +22,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Divider, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -75,21 +77,21 @@ const Main = () => {
return (
<>
{popup}
<p className="text-sm">
<Text>
This connector allows you to sync all your Gong Transcripts into
Danswer. More details on how to setup the Gong connector can be found in{" "}
<a
className="text-blue-500"
className="text-link"
href="https://docs.danswer.dev/connectors/gong"
target="_blank"
>
this guide.
</a>
</p>
</Text>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your API Access info
</h2>
</Title>
{gongCredential ? (
<>
@ -99,7 +101,7 @@ const Main = () => {
{gongCredential.credential_json?.gong_access_key_secret}
</p>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (gongConnectorIndexingStatuses.length > 0) {
setPopup({
@ -119,7 +121,7 @@ const Main = () => {
</>
) : (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
<Card className="mt-4">
<CredentialForm<GongCredentialJson>
formBody={
<>
@ -149,19 +151,19 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Which Workspaces do you want to make searchable?
</h2>
</Title>
{gongConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We pull the latest transcript every <b>10</b> minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<GongConfig, GongCredentialJson>
connectorIndexingStatuses={gongConnectorIndexingStatuses}
@ -196,12 +198,13 @@ const Main = () => {
}}
/>
</div>
<Divider />
</>
)}
{gongCredential ? (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Create a new Gong Connector</h2>
<ConnectorForm<GongConfig>
nameBuilder={(values) =>
@ -230,13 +233,13 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={gongCredential.id}
/>
</div>
</Card>
</>
) : (
<p className="text-sm">
<Text>
Please provide your API Access Info in Step 1 first! Once done with
that, you can then start indexing all your Gong transcripts.
</p>
</Text>
)}
</>
);
@ -248,10 +251,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GongIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Gong</h1>
</div>
<AdminPageTitle icon={<GongIcon size={32} />} title="Gong" />
<Main />
</div>
);

View File

@ -7,6 +7,8 @@ import { XIcon } from "@/components/icons/icons";
import { Connector, GoogleDriveConfig } from "@/lib/types";
import * as Yup from "yup";
import { googleDriveConnectorNameBuilder } from "./utils";
import { Modal } from "@/components/Modal";
import { Divider, Text } from "@tremor/react";
interface Props {
existingConnector: Connector<GoogleDriveConfig>;
@ -15,25 +17,28 @@ interface Props {
export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
return (
<div
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
onClick={onSubmit}
>
<div
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
>
<div className="flex border-b border-gray-600 pb-2 mb-2">
<h3 className="text-lg font-semibold w-full">
<Modal onOutsideClick={onSubmit}>
<div className="px-8 py-6 bg-background">
<h2 className="text-xl font-bold flex">
Update Google Drive Connector
</h3>
<div onClick={onSubmit}>
<div
onClick={onSubmit}
className="ml-auto hover:bg-hover p-1.5 rounded"
>
<XIcon
size={30}
className="my-auto flex flex-shrink-0 cursor-pointer hover:text-blue-400"
size={20}
className="my-auto flex flex-shrink-0 cursor-pointer"
/>
</div>
</div>
</h2>
<Text>
Modify the selected Google Drive connector by adjusting the values
below!
</Text>
<Divider />
<UpdateConnectorForm<GoogleDriveConfig>
nameBuilder={googleDriveConnectorNameBuilder}
existingConnector={existingConnector}
@ -67,6 +72,6 @@ export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
onSubmit={onSubmit}
/>
</div>
</div>
</Modal>
);
};

View File

@ -15,6 +15,7 @@ import { GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
import Cookies from "js-cookie";
import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik } from "formik";
import { Card } from "@tremor/react";
type GoogleDriveCredentialJsonTypes = "authorized_user" | "service_account";
@ -246,7 +247,7 @@ export const DriveJsonUploadSection = ({
<p className="text-sm mb-2">
Follow the guide{" "}
<a
className="text-blue-500"
className="text-link"
target="_blank"
href="https://docs.danswer.dev/connectors/google_drive#authorization"
>
@ -322,7 +323,7 @@ export const DriveOAuthSection = ({
the documents you want to index with the service account.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
<Card>
<Formik
initialValues={{
google_drive_delegated_user: "",
@ -385,7 +386,7 @@ export const DriveOAuthSection = ({
</Form>
)}
</Formik>
</div>
</Card>
</div>
);
}

View File

@ -13,6 +13,14 @@ import { useSWRConfig } from "swr";
import { useState } from "react";
import { ConnectorEditPopup } from "./ConnectorEditPopup";
import { DeleteColumn } from "@/components/admin/connectors/table/DeleteColumn";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
interface EditableColumnProps {
connectorIndexingStatus: ConnectorIndexingStatus<
@ -44,7 +52,7 @@ const EditableColumn = ({ connectorIndexingStatus }: EditableColumnProps) => {
className="cursor-pointer"
>
<div className="mr-2">
<EditIcon size={20} />
<EditIcon size={16} />
</div>
</div>
</div>
@ -74,6 +82,99 @@ export const GoogleDriveConnectorsTable = ({
(a, b) => a.connector.id - b.connector.id
);
return (
<div>
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Edit</TableHeaderCell>
<TableHeaderCell>Folder Paths</TableHeaderCell>
<TableHeaderCell>Include Shared</TableHeaderCell>
<TableHeaderCell>Follow Shortcuts</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Delete</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{sortedGoogleDriveConnectorIndexingStatuses.map(
(connectorIndexingStatus) => {
return (
<TableRow key={connectorIndexingStatus.cc_pair_id}>
<TableCell>
<EditableColumn
connectorIndexingStatus={connectorIndexingStatus}
/>
</TableCell>
<TableCell>
{(
connectorIndexingStatus.connector
.connector_specific_config.folder_paths || []
).length > 0 ? (
<div key={connectorIndexingStatus.connector.id}>
{(
connectorIndexingStatus.connector
.connector_specific_config.folder_paths || []
).map((path) => (
<div key={path}>
<i> - {path}</i>
</div>
))}
</div>
) : (
<i>All Folders</i>
)}
</TableCell>
<TableCell>
<div>
{connectorIndexingStatus.connector
.connector_specific_config.include_shared ? (
<i>Yes</i>
) : (
<i>No</i>
)}
</div>
</TableCell>
<TableCell>
<div>
{connectorIndexingStatus.connector
.connector_specific_config.follow_shortcuts ? (
<i>Yes</i>
) : (
<i>No</i>
)}
</div>
</TableCell>
<TableCell>
<StatusRow
connectorIndexingStatus={connectorIndexingStatus}
hasCredentialsIssue={
connectorIndexingStatus.connector.credential_ids
.length === 0
}
setPopup={setPopup}
onUpdate={() => {
mutate("/api/manage/admin/connector/indexing-status");
}}
/>
</TableCell>
<TableCell>
<DeleteColumn
connectorIndexingStatus={connectorIndexingStatus}
setPopup={setPopup}
onUpdate={() =>
mutate("/api/manage/admin/connector/indexing-status")
}
/>
</TableCell>
</TableRow>
);
}
)}
</TableBody>
</Table>
</div>
);
return (
<BasicTable
columns={[

View File

@ -24,6 +24,8 @@ import { GoogleDriveConnectorsTable } from "./GoogleDriveConnectorsTable";
import { googleDriveConnectorNameBuilder } from "./utils";
import { DriveOAuthSection, DriveJsonUploadSection } from "./Credential";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Divider, Text, Title } from "@tremor/react";
interface GoogleDriveConnectorManagementProps {
googleDrivePublicCredential?: Credential<GoogleDriveCredentialJson>;
@ -54,10 +56,10 @@ const GoogleDriveConnectorManagement = ({
googleDrivePublicCredential || googleDriveServiceAccountCredential;
if (!liveCredential) {
return (
<p className="text-sm">
<Text>
Please authenticate with Google Drive as described in Step 2! Once done
with that, you can then move on to enable this connector.
</p>
</Text>
);
}
@ -151,7 +153,7 @@ const GoogleDriveConnectorManagement = ({
return (
<div>
<div className="text-sm">
<Text>
<div className="my-3">
{googleDriveConnectorIndexingStatuses.length > 0 ? (
<>
@ -169,7 +171,7 @@ const GoogleDriveConnectorManagement = ({
</p>
)}
</div>
</div>
</Text>
{googleDriveConnectorIndexingStatuses.length > 0 && (
<>
<div className="text-sm mb-2 font-bold">Existing Connectors:</div>
@ -179,13 +181,14 @@ const GoogleDriveConnectorManagement = ({
}
setPopup={setPopup}
/>
<Divider />
</>
)}
{googleDriveConnectorIndexingStatuses.length > 0 && (
<h2 className="font-bold mt-3 text-sm">Add New Connector:</h2>
)}
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
<Card className="mt-4">
<ConnectorForm<GoogleDriveConfig>
nameBuilder={googleDriveConnectorNameBuilder}
source="google_drive"
@ -239,7 +242,7 @@ const GoogleDriveConnectorManagement = ({
refreshFreq={10 * 60} // 10 minutes
credentialId={liveCredential.id}
/>
</div>
</Card>
</div>
);
};
@ -353,18 +356,18 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
<DriveJsonUploadSection
setPopup={setPopup}
appCredentialData={appCredentialData}
serviceAccountCredentialData={serviceAccountKeyData}
/>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Authenticate with Danswer
</h2>
</Title>
<DriveOAuthSection
setPopup={setPopup}
refreshCredentials={refreshCredentials}
@ -376,9 +379,9 @@ const Main = () => {
serviceAccountKeyData={serviceAccountKeyData}
/>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 3: Start Indexing!
</h2>
</Title>
<GoogleDriveConnectorManagement
googleDrivePublicCredential={googleDrivePublicCredential}
googleDriveServiceAccountCredential={
@ -401,10 +404,11 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GoogleDriveIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Google Drive</h1>
</div>
<AdminPageTitle
icon={<GoogleDriveIcon size={32} />}
title="Google Drive"
/>
<Main />
</div>

View File

@ -17,6 +17,8 @@ import { linkCredential } from "@/lib/credential";
import { FileUpload } from "@/components/admin/connectors/FileUpload";
import { SingleUseConnectorsTable } from "@/components/admin/connectors/table/SingleUseConnectorsTable";
import { Spinner } from "@/components/Spinner";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button, Card, Text, Title } from "@tremor/react";
export default function GoogleSites() {
const { mutate } = useSWRConfig();
@ -50,11 +52,13 @@ export default function GoogleSites() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<GoogleSitesIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Google Sites</h1>
</div>
<p className="text-sm mb-2">
<AdminPageTitle
icon={<GoogleSitesIcon size={32} />}
title="Google Sites"
/>
<Text className="mb-2">
For an in-depth guide on how to setup this connector, check out{" "}
<a
href="https://docs.danswer.dev/connectors/google_sites"
@ -64,10 +68,11 @@ export default function GoogleSites() {
the documentation
</a>
.
</p>
</Text>
<div className="mt-4">
<h2 className="font-bold text-xl mb-2">Upload Files</h2>
<Title className="mb-2">Upload Files</Title>
<Card>
<div className="mx-auto w-full">
<Formik
initialValues={{
@ -164,7 +169,7 @@ export default function GoogleSites() {
}}
>
{({ values, isSubmitting }) => (
<Form className="p-3 border border-gray-600 rounded">
<Form>
<TextFormField
name="base_url"
label="Base URL:"
@ -179,12 +184,11 @@ export default function GoogleSites() {
setSelectedFiles={setSelectedFiles}
message="Upload a zip file containing the HTML of your Google Site"
/>
<button
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full mx-auto mt-4"
}
<div className="flex">
<Button
className="mt-4 w-64 mx-auto"
size="xs"
color="green"
type="submit"
disabled={
selectedFiles.length !== 1 ||
@ -193,11 +197,13 @@ export default function GoogleSites() {
}
>
Upload!
</button>
</Button>
</div>
</Form>
)}
</Formik>
</div>
</Card>
</div>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Text, Title } from "@tremor/react";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -70,23 +72,23 @@ const Main = () => {
return (
<>
{popup}
<p className="text-sm">
<Text>
This connector allows you to sync all your Guru Cards into Danswer.
</p>
</Text>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
{guruCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Access Token: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
<Text className="my-auto">Existing Access Token: </Text>
<Text className="ml-1 italic my-auto max-w-md truncate">
{guruCredential.credential_json?.guru_user_token}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (guruConnectorIndexingStatuses.length > 0) {
setPopup({
@ -106,18 +108,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the Guru connector, first follow the guide{" "}
<a
className="text-blue-500"
className="text-link"
href="https://help.getguru.com/s/article/how-to-obtain-your-api-credentials"
target="_blank"
>
here
</a>{" "}
to generate a User Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<GuruCredentialJson>
formBody={
<>
@ -147,21 +149,21 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Start indexing!
</h2>
</Title>
{guruCredential ? (
!guruConnectorIndexingStatuses.length ? (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
Click the button below to start indexing! We will pull the latest
features, components, and products from Guru every <b>10</b>{" "}
minutes.
</p>
</Text>
<div className="flex">
<ConnectorForm<GuruConfig>
nameBuilder={() => "GuruConnector"}
@ -178,10 +180,10 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
Guru connector is setup! We are pulling the latest cards from Guru
every <b>10</b> minutes.
</p>
</Text>
<ConnectorsTable<GuruConfig, GuruCredentialJson>
connectorIndexingStatuses={guruConnectorIndexingStatuses}
liveCredential={guruCredential}
@ -206,10 +208,10 @@ const Main = () => {
)
) : (
<>
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with
that, you can then start indexing all your Guru cards.
</p>
</Text>
</>
)}
</>
@ -222,10 +224,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GuruIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Guru</h1>
</div>
<AdminPageTitle icon={<GuruIcon size={32} />} title="Guru" />
<Main />
</div>
);

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Text, Title } from "@tremor/react";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -72,23 +74,23 @@ const Main = () => {
return (
<>
{popup}
<p className="text-sm">
<Text>
This connector allows you to sync all your HubSpot Tickets into Danswer.
</p>
</Text>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
{hubSpotCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Access Token: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
<Text className="my-auto">Existing Access Token: </Text>
<Text className="ml-1 italic my-auto max-w-md truncate">
{hubSpotCredential.credential_json?.hubspot_access_token}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (hubSpotConnectorIndexingStatuses.length > 0) {
setPopup({
@ -108,10 +110,10 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the HubSpot connector, provide the HubSpot Access Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<HubSpotCredentialJson>
formBody={
<>
@ -136,20 +138,20 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Start indexing!
</h2>
</Title>
{hubSpotCredential ? (
!hubSpotConnectorIndexingStatuses.length ? (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
Click the button below to start indexing! We will pull the latest
tickets from HubSpot every <b>10</b> minutes.
</p>
</Text>
<div className="flex">
<ConnectorForm<HubSpotConfig>
nameBuilder={() => "HubSpotConnector"}
@ -170,10 +172,10 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
HubSpot connector is setup! We are pulling the latest tickets from
HubSpot every <b>10</b> minutes.
</p>
</Text>
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}
liveCredential={hubSpotCredential}
@ -198,10 +200,10 @@ const Main = () => {
)
) : (
<>
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with
that, you can then start indexing all your HubSpot tickets.
</p>
</Text>
</>
)}
</>
@ -214,10 +216,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<HubSpotIcon size={32} />
<h1 className="text-3xl font-bold pl-2">HubSpot</h1>
</div>
<AdminPageTitle icon={<HubSpotIcon size={32} />} title="HubSpot" />
<Main />
</div>
);

View File

@ -6,7 +6,6 @@ import { TextFormField } from "@/components/admin/connectors/Field";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
import {
Credential,
JiraConfig,
JiraCredentialJson,
ConnectorIndexingStatus,
@ -19,6 +18,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Divider, Text, Title } from "@tremor/react";
// Copied from the `extract_jira_project` function
const extractJiraProject = (url: string): string | null => {
@ -82,9 +83,9 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
{jiraCredential ? (
<>
@ -132,17 +133,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the Jira connector, first follow the guide{" "}
<a
className="text-blue-500"
className="text-link"
href="https://docs.danswer.dev/connectors/jira#setting-up"
target="_blank"
>
here
</a>{" "}
to generate an Access Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<JiraCredentialJson>
formBody={
<>
@ -172,18 +174,18 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
{/* TODO: make this periodic */}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Which spaces do you want to make searchable?
</h2>
</Title>
{jiraCredential ? (
<>
{" "}
<p className="text-sm mb-4">
<Text className="mb-4">
Specify any link to a Jira page below and click &quot;Index&quot; to
Index. Based on the provided link, we will index the ENTIRE PROJECT,
not just the specified page. For example, entering{" "}
@ -192,13 +194,13 @@ const Main = () => {
</i>{" "}
and clicking the Index button will index the whole <i>DAN</i> Jira
project.
</p>
</Text>
{jiraConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We pull the latest pages and comments from each space listed
below every <b>10</b> minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<JiraConfig, JiraCredentialJson>
connectorIndexingStatuses={jiraConnectorIndexingStatuses}
@ -239,9 +241,10 @@ const Main = () => {
}
/>
</div>
<Divider />
</>
)}
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Add a New Project</h2>
<ConnectorForm<JiraConfig>
nameBuilder={(values) =>
@ -271,15 +274,15 @@ const Main = () => {
}}
refreshFreq={10 * 60} // 10 minutes
/>
</div>
</Card>
</>
) : (
<>
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with
that, you can then specify which Jira projects you want to make
searchable.
</p>
</Text>
</>
)}
</>
@ -292,10 +295,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<JiraIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Jira</h1>
</div>
<AdminPageTitle icon={<JiraIcon size={32} />} title="Jira" />
<Main />
</div>
);

View File

@ -18,6 +18,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -70,19 +72,19 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
{linearCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing API Key: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
<Text className="my-auto">Existing API Key: </Text>
<Text className="ml-1 italic my-auto max-w-md truncate">
{linearCredential.credential_json?.linear_api_key}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (linearConnectorIndexingStatuses.length > 0) {
setPopup({
@ -102,17 +104,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the Linear connector, first follow the guide{" "}
<a
className="text-blue-500"
href="https://docs.danswer.dev/connectors/linear"
target="_blank"
>
here
</a>{" "}
to generate an API Key.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<LinearCredentialJson>
formBody={
<>
@ -137,21 +140,21 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Start indexing
</h2>
</Title>
{linearCredential ? (
<>
{linearConnectorIndexingStatuses.length > 0 ? (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We pull the latest <i>issues</i> and <i>comments</i> every{" "}
<b>10</b> minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<{}, LinearCredentialJson>
connectorIndexingStatuses={linearConnectorIndexingStatuses}
@ -176,7 +179,7 @@ const Main = () => {
</div>
</>
) : (
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Create Connector</h2>
<p className="text-sm mb-4">
Press connect below to start the connection Linear. We pull the
@ -194,15 +197,15 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={linearCredential.id}
/>
</div>
</Card>
)}
</>
) : (
<>
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with
that, you can then start indexing Linear.
</p>
</Text>
</>
)}
</>
@ -215,10 +218,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<LinearIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Linear</h1>
</div>
<AdminPageTitle icon={<LinearIcon size={32} />} title="Linear" />
<Main />
</div>
);

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Divider, Text, Title } from "@tremor/react";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -69,9 +71,9 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your authorization details
</h2>
</Title>
{notionCredential ? (
<>
@ -101,7 +103,7 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To get started you&apos;ll need to create an internal integration in
Notion for Danswer. Follow the instructions in the&nbsp;
<a
@ -115,8 +117,8 @@ const Main = () => {
token and paste it below. Follow the remaining instructions on the
Notion docs to allow Danswer to read Notion Databases and Pages
using the new integration.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
</Text>
<Card className="mt-2 mb-4">
<CredentialForm<NotionCredentialJson>
formBody={
<TextFormField
@ -140,18 +142,18 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Manage Connectors
</h2>
</Title>
{notionConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
The latest page updates are fetched from Notion every 10 minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<NotionConfig, NotionCredentialJson>
connectorIndexingStatuses={notionConnectorIndexingStatuses}
@ -183,12 +185,13 @@ const Main = () => {
}
/>
</div>
<Divider />
</>
)}
{notionCredential && (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-1">Create New Connection</h2>
<p className="text-sm mb-4">
Press connect below to start the connection to Notion.
@ -226,17 +229,17 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={notionCredential.id}
/>
</div>
</Card>
</>
)}
{!notionCredential && (
<>
<p className="text-sm mb-4">
<Text className="mb-4">
Please provide your integration details in Step 1 first! Once done
with that, you&apos;ll be able to start the connection then see
indexing status.
</p>
</Text>
</>
)}
</>
@ -249,10 +252,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<NotionIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Notion</h1>
</div>
<AdminPageTitle icon={<NotionIcon size={32} />} title="Notion" />
<Main />
</div>
);

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -72,29 +74,29 @@ const Main = () => {
return (
<>
{popup}
<p className="text-sm">
<Text>
This connector allows you to sync all your <i>Features</i>,{" "}
<i>Components</i>, <i>Products</i>, and <i>Objectives</i> from
Productboard into Danswer. At this time, the Productboard APIs does not
support pulling in <i>Releases</i> or <i>Notes</i>.
</p>
</Text>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
{productboardCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Access Token: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
<Text className="my-auto">Existing Access Token: </Text>
<Text className="ml-1 italic my-auto max-w-md truncate">
{
productboardCredential.credential_json
?.productboard_access_token
}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (productboardConnectorIndexingStatuses.length > 0) {
setPopup({
@ -114,17 +116,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the Productboard connector, first follow the guide{" "}
<a
className="text-blue-500"
className="text-link"
href="https://developer.productboard.com/#section/Authentication/Public-API-Access-Token"
target="_blank"
>
here
</a>{" "}
to generate an Access Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<ProductboardCredentialJson>
formBody={
<>
@ -149,21 +152,21 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Start indexing!
</h2>
</Title>
{productboardCredential ? (
!productboardConnectorIndexingStatuses.length ? (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
Click the button below to start indexing! We will pull the latest
features, components, and products from Productboard every{" "}
<b>10</b> minutes.
</p>
</Text>
<div className="flex">
<ConnectorForm<ProductboardConfig>
nameBuilder={() => "ProductboardConnector"}
@ -180,11 +183,11 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
Productboard connector is setup! We are pulling the latest
features, components, and products from Productboard every{" "}
<b>10</b> minutes.
</p>
</Text>
<ConnectorsTable<ProductboardConfig, ProductboardCredentialJson>
connectorIndexingStatuses={productboardConnectorIndexingStatuses}
liveCredential={productboardCredential}
@ -211,11 +214,11 @@ const Main = () => {
)
) : (
<>
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with
that, you can then start indexing all your Productboard features,
components, and products.
</p>
</Text>
</>
)}
</>
@ -228,10 +231,12 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<ProductboardIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Productboard</h1>
</div>
<AdminPageTitle
icon={<ProductboardIcon size={32} />}
title="Productboard"
/>
<Main />
</div>
);

View File

@ -21,6 +21,8 @@ import {
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Text, Title } from "@tremor/react";
const MainSection = () => {
const { mutate } = useSWRConfig();
@ -71,18 +73,18 @@ const MainSection = () => {
return (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide Request Tracker credentials
</h2>
</Title>
{requestTrackerCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Request Tracker username: </p>
<p className="ml-1 italic my-auto">
<Text className="my-auto">Existing Request Tracker username: </Text>
<Text className="ml-1 italic my-auto">
{requestTrackerCredential.credential_json.requesttracker_username}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
await adminDeleteCredential(requestTrackerCredential.id);
refreshCredentials();
@ -94,20 +96,24 @@ const MainSection = () => {
</>
) : (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
To use the Request Tracker connector, provide a Request Tracker
username, password, and base url.
</p>
<p className="text-sm mb-2">
</Text>
<Text className="mb-2">
This connector currently supports{" "}
<a href="https://rt-wiki.bestpractical.com/wiki/REST">
<a
className="text-link"
href="https://rt-wiki.bestpractical.com/wiki/REST"
target="_blank"
>
Request Tracker REST API 1.0
</a>
,{" "}
<b>not the latest REST API 2.0 introduced in Request Tracker 5.0</b>
.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-2">
<CredentialForm<RequestTrackerCredentialJson>
formBody={
<>
@ -150,21 +156,25 @@ const MainSection = () => {
}
}}
/>
</div>
</Card>
</>
)}
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Manage Request Tracker Connector
</Title>
{requestTrackerConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We index the most recently updated tickets from each Request Tracker
instance listed below regularly.
</p>
<p className="text-sm mb-2">
</Text>
<Text className="mb-2">
The initial poll at this time retrieves tickets updated in the past
hour. All subsequent polls execute every ten minutes. This should be
configurable in the future.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<RequestTrackerConfig, RequestTrackerCredentialJson>
connectorIndexingStatuses={
@ -193,10 +203,7 @@ const MainSection = () => {
{requestTrackerCredential &&
requestTrackerConnectorIndexingStatuses.length === 0 ? (
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<h2 className="font-bold mb-3">
Step 2: (Re)initialize connection to Request Tracker installation
</h2>
<Card className="mt-4">
<ConnectorForm<RequestTrackerConfig>
nameBuilder={(values) =>
`RequestTracker-${requestTrackerCredential.credential_json.requesttracker_base_url}`
@ -212,7 +219,7 @@ const MainSection = () => {
credentialId={requestTrackerCredential.id}
refreshFreq={10 * 60} // 10 minutes
/>
</div>
</Card>
) : (
<></>
)}
@ -226,10 +233,12 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<RequestTrackerIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Request Tracker</h1>
</div>
<AdminPageTitle
icon={<RequestTrackerIcon size={32} />}
title="Request Tracker"
/>
<MainSection />
</div>
);

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -71,19 +73,19 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</h2>
</Title>
{slabCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Slab Bot Token: </p>
<p className="ml-1 italic my-auto max-w-md truncate">
<Text className="my-auto">Existing Slab Bot Token: </Text>
<Text className="ml-1 italic my-auto max-w-md truncate">
{slabCredential.credential_json?.slab_bot_token}
</p>
</Text>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (slabConnectorIndexingStatuses.length > 0) {
setPopup({
@ -103,17 +105,18 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To use the Slab connector, first follow the guide{" "}
<a
className="text-blue-500"
className="text-link"
href="https://docs.danswer.dev/connectors/slab"
target="_blank"
>
here
</a>{" "}
to generate a Slab Bot Token.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="p-6 mt-2">
<CredentialForm<SlabCredentialJson>
formBody={
<>
@ -138,18 +141,18 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: What&apos;s the base URL for your Slab team?
</h2>
</Title>
{slabCredential ? (
<>
{slabConnectorIndexingStatuses.length > 0 ? (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We are pulling the latest documents from{" "}
<a
href={
@ -164,7 +167,7 @@ const Main = () => {
}
</a>{" "}
every <b>10</b> minutes.
</p>
</Text>
<ConnectorsTable<SlabConfig, SlabCredentialJson>
connectorIndexingStatuses={slabConnectorIndexingStatuses}
liveCredential={slabCredential}
@ -206,14 +209,14 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm mb-4">
<Text className="mb-4">
Specify the base URL for your Slab team below. This will look
something like:{" "}
<b>
<i>https://danswer.slab.com/</i>
</b>
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
</Text>
<Card className="mt-4">
<h2 className="font-bold mb-3">Add a New Space</h2>
<ConnectorForm<SlabConfig>
nameBuilder={(values) => `SlabConnector-${values.base_url}`}
@ -236,17 +239,17 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={slabCredential.id}
/>
</div>
</Card>
</>
)}
</>
) : (
<>
<p className="text-sm">
<Text>
Please provide your access token in Step 1 first! Once done with
that, you can then specify the URL for your Slab team and get
started with indexing.
</p>
</Text>
</>
)}
</>
@ -259,10 +262,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<SlabIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Slab</h1>
</div>
<AdminPageTitle icon={<SlabIcon size={32} />} title="Slab" />
<Main />
</div>
);

View File

@ -21,6 +21,8 @@ import {
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { usePublicCredentials } from "@/lib/hooks";
import { Button, Card, Divider, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const MainSection = () => {
const { mutate } = useSWRConfig();
@ -69,25 +71,27 @@ const MainSection = () => {
return (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide Credentials
</h2>
</Title>
{slackCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing Slack Bot Token: </p>
<p className="ml-1 italic my-auto">
<Text className="my-auto">Existing Slack Bot Token: </Text>
<Text className="ml-1 italic my-auto">
{slackCredential.credential_json.slack_bot_token}
</p>{" "}
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
</Text>
<Button
size="xs"
color="red"
className="ml-3 text-inverted"
onClick={async () => {
await adminDeleteCredential(slackCredential.id);
refreshCredentials();
}}
>
<TrashIcon />
</button>
</Button>
</div>
</>
) : (
@ -104,7 +108,7 @@ const MainSection = () => {
</a>
.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
<Card>
<CredentialForm<SlackCredentialJson>
formBody={
<>
@ -129,20 +133,20 @@ const MainSection = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Which channels do you want to make searchable?
</h2>
</Title>
{slackConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We pull the latest messages from each workspace listed below every{" "}
<b>10</b> minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<SlackConfig, SlackCredentialJson>
connectorIndexingStatuses={slackConnectorIndexingStatuses}
@ -181,11 +185,12 @@ const MainSection = () => {
}}
/>
</div>
<Divider />
</>
)}
{slackCredential ? (
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card>
<h2 className="font-bold mb-3">Connect to a New Workspace</h2>
<ConnectorForm<SlackConfig>
nameBuilder={(values) =>
@ -226,13 +231,13 @@ const MainSection = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={slackCredential.id}
/>
</div>
</Card>
) : (
<p className="text-sm">
<Text>
Please provide your slack bot token in Step 1 first! Once done with
that, you can then specify which Slack channels you want to make
searchable.
</p>
</Text>
)}
</>
);
@ -244,10 +249,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<SlackIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Slack</h1>
</div>
<AdminPageTitle icon={<SlackIcon size={32} />} title="Slack" />
<MainSection />
</div>
);

View File

@ -18,7 +18,8 @@ import { HealthCheckBanner } from "@/components/health/healthcheck";
import { ConnectorIndexingStatus, WebConfig } from "@/lib/types";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { createCredential, linkCredential } from "@/lib/credential";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Title } from "@tremor/react";
const SCRAPE_TYPE_TO_PRETTY_NAME = {
recursive: "Recursive",
@ -49,17 +50,16 @@ export default function Web() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<GlobeIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Web</h1>
</div>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<AdminPageTitle icon={<GlobeIcon size={32} />} title="Web" />
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Specify which websites to index
</h2>
</Title>
<p className="text-sm mb-2">
We re-fetch the latest state of the website once a day.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6">
<Card>
<ConnectorForm<WebConfig>
nameBuilder={(values) => `WebConnector-${values.base_url}`}
ccPairNameBuilder={(values) => values.base_url}
@ -118,11 +118,11 @@ export default function Web() {
}}
refreshFreq={60 * 60 * 24} // 1 day
/>
</div>
</Card>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Already Indexed Websites
</h2>
</Title>
{isConnectorIndexingStatusesLoading ? (
<LoadingAnimation text="Loading" />
) : isConnectorIndexingStatusesError || !connectorIndexingStatuses ? (

View File

@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Divider, Text, Title } from "@tremor/react";
const Main = () => {
const { popup, setPopup } = usePopup();
@ -69,9 +71,9 @@ const Main = () => {
return (
<>
{popup}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your API details
</h2>
<Title className="mb-2 mt-6 ml-auto mr-auto">
Provide your API details
</Title>
{zendeskCredential ? (
<>
@ -81,7 +83,7 @@ const Main = () => {
{zendeskCredential.credential_json?.zendesk_email}
</p>
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
if (zendeskConnectorIndexingStatuses.length > 0) {
setPopup({
@ -101,7 +103,7 @@ const Main = () => {
</>
) : (
<>
<p className="text-sm">
<Text>
To get started you&apos;ll need API token details for your Zendesk
instance. You can generate this by access the Admin Center of your
instance (e.g. https://&lt;subdomain&gt;.zendesk.com/admin/).
@ -110,8 +112,8 @@ const Main = () => {
with a name. You will also need to provide the e-mail address of a
user that the system will impersonate. This is of little consequence
as we are only performing read actions.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
</Text>
<Card className="mt-4 mb-4">
<CredentialForm<ZendeskCredentialJson>
formBody={
<>
@ -153,18 +155,18 @@ const Main = () => {
}
}}
/>
</div>
</Card>
</>
)}
{zendeskConnectorIndexingStatuses.length > 0 && (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Zendesk indexing status
</h2>
<p className="text-sm mb-2">
</Title>
<Text className="mb-2">
The latest article changes are fetched every 10 minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable<ZendeskConfig, ZendeskCredentialJson>
connectorIndexingStatuses={zendeskConnectorIndexingStatuses}
@ -192,7 +194,7 @@ const Main = () => {
{zendeskCredential && zendeskConnectorIndexingStatuses.length === 0 && (
<>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Create Connection</h2>
<p className="text-sm mb-4">
Press connect below to start the connection to your Zendesk
@ -209,17 +211,17 @@ const Main = () => {
refreshFreq={10 * 60} // 10 minutes
credentialId={zendeskCredential.id}
/>
</div>
</Card>
</>
)}
{!zendeskCredential && (
<>
<p className="text-sm mb-4">
<Text className="mb-4">
Please provide your API details in Step 1 first! Once done with
that, you&apos;ll be able to start the connection then see indexing
status.
</p>
</Text>
</>
)}
</>
@ -232,10 +234,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<ZendeskIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Zendesk</h1>
</div>
<AdminPageTitle icon={<ZendeskIcon size={32} />} title="Zendesk" />
<Main />
</div>
);

View File

@ -18,6 +18,8 @@ import { TextFormField } from "@/components/admin/connectors/Field";
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
import { usePublicCredentials } from "@/lib/hooks";
import { Card, Divider, Text, Title } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const MainSection = () => {
const { mutate } = useSWRConfig();
@ -66,18 +68,18 @@ const MainSection = () => {
return (
<>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide Credentials
</h2>
</Title>
{zulipCredential ? (
<>
<div className="flex mb-1 text-sm">
<p className="my-auto">Existing zuliprc file content: </p>
<p className="ml-1 italic my-auto">
<Text className="my-auto">Existing zuliprc file content: </Text>
<Text className="ml-1 italic my-auto">
{zulipCredential.credential_json.zuliprc_content}
</p>{" "}
</Text>{" "}
<button
className="ml-1 hover:bg-gray-700 rounded-full p-1"
className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => {
await adminDeleteCredential(zulipCredential.id);
refreshCredentials();
@ -89,19 +91,20 @@ const MainSection = () => {
</>
) : (
<>
<p className="text-sm mb-4">
<Text className="mb-4">
To use the Zulip connector, you must first provide content of the
zuliprc config file. For more details on setting up the Danswer
Zulip connector, see the{" "}
<a
className="text-blue-500"
className="text-link"
href="https://docs.danswer.dev/connectors/zulip"
target="_blank"
>
docs
</a>
.
</p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
</Text>
<Card className="mt-4">
<CredentialForm<ZulipCredentialJson>
formBody={
<>
@ -126,22 +129,22 @@ const MainSection = () => {
}
}}
/>
</div>
</Card>
</>
)}
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Which workspaces do you want to make searchable?
</h2>
</Title>
{zulipCredential ? (
<>
{zulipConnectorIndexingStatuses.length > 0 && (
<>
<p className="text-sm mb-2">
<Text className="mb-2">
We pull the latest messages from each workspace listed below
every <b>10</b> minutes.
</p>
</Text>
<div className="mb-2">
<ConnectorsTable
connectorIndexingStatuses={zulipConnectorIndexingStatuses}
@ -176,10 +179,11 @@ const MainSection = () => {
}}
/>
</div>
<Divider />
</>
)}
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
<Card className="mt-4">
<h2 className="font-bold mb-3">Connect to a New Realm</h2>
<ConnectorForm<ZulipConfig>
nameBuilder={(values) => `ZulipConnector-${values.realm_name}`}
@ -205,14 +209,14 @@ const MainSection = () => {
}}
refreshFreq={10 * 60} // 10 minutes
/>
</div>
</Card>
</>
) : (
<p className="text-sm">
<Text>
Please provide your Zulip credentials in Step 1 first! Once done with
that, you can then specify which Zulip realms you want to make
searchable.
</p>
</Text>
)}
</>
);
@ -224,10 +228,9 @@ export default function Page() {
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<ZulipIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Zulip</h1>
</div>
<AdminPageTitle icon={<ZulipIcon size={32} />} title="Zulip" />
<MainSection />
</div>
);

View File

@ -2,6 +2,7 @@ import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import { updateBoost } from "./lib";
import { CheckmarkIcon, EditIcon } from "@/components/icons/icons";
import { FiEdit } from "react-icons/fi";
export const ScoreSection = ({
documentId,
@ -62,7 +63,7 @@ export const ScoreSection = ({
setScore(initialScore.toString());
}
}}
className="border bg-slate-700 text-gray-200 border-gray-300 rounded py-1 px-3 w-16 h-5 my-auto"
className="border bg-background-strong border-gray-300 rounded py-1 px-1 w-12 h-4 my-auto"
/>
<div onClick={onSubmit} className="cursor-pointer my-auto ml-2">
<CheckmarkIcon size={16} className="text-green-700" />
@ -73,15 +74,15 @@ export const ScoreSection = ({
return (
<div className="h-full flex flex-col">
<div className="flex my-auto">
<div className={"flex" + (consistentWidth && " w-6")}>
<div className="ml-auto my-auto">{initialScore}</div>
</div>
<div
className="cursor-pointer ml-2 my-auto"
className="flex my-auto cursor-pointer hover:bg-hover rounded"
onClick={() => setIsOpen(true)}
>
<EditIcon size={16} />
<div className={"flex " + (consistentWidth && " w-6")}>
<div className="ml-auto my-auto">{initialScore}</div>
</div>
<div className="cursor-pointer ml-2 my-auto h-4">
<FiEdit size={16} />
</div>
</div>
</div>

View File

@ -30,7 +30,7 @@ const DocumentDisplay = ({
return (
<div
key={document.document_id}
className="text-sm border-b border-gray-800 mb-3"
className="text-sm border-b border-border mb-3"
>
<div className="flex relative">
<a
@ -49,7 +49,7 @@ const DocumentDisplay = ({
</a>
</div>
<div className="flex flex-wrap gap-x-2 mt-1 text-xs">
<div className="px-1 py-0.5 bg-gray-700 rounded flex">
<div className="px-1 py-0.5 bg-hover rounded flex">
<p className="mr-1 my-auto">Boost:</p>
<ScoreSection
documentId={document.document_id}
@ -76,11 +76,11 @@ const DocumentDisplay = ({
});
}
}}
className="px-1 py-0.5 bg-gray-700 hover:bg-gray-600 rounded flex cursor-pointer select-none"
className="px-1 py-0.5 bg-hover hover:bg-hover-light rounded flex cursor-pointer select-none"
>
<div className="my-auto">
{document.hidden ? (
<div className="text-red-500">Hidden</div>
<div className="text-error">Hidden</div>
) : (
"Visible"
)}
@ -95,7 +95,7 @@ const DocumentDisplay = ({
<DocumentUpdatedAtBadge updatedAt={document.updated_at} />
</div>
)}
<p className="pl-1 pt-2 pb-3 text-gray-200 break-words">
<p className="pl-1 pt-2 pb-3 break-words">
{buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
</p>
</div>
@ -159,11 +159,11 @@ export function Explorer({
<div>
{popup}
<div className="justify-center py-2">
<div className="flex items-center w-full border-2 border-gray-600 rounded px-4 py-2 focus-within:border-blue-500">
<MagnifyingGlass className="text-gray-400" />
<div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent">
<MagnifyingGlass />
<textarea
autoFocus
className="flex-grow ml-2 h-6 bg-transparent outline-none placeholder-gray-400 overflow-hidden whitespace-normal resize-none"
className="flex-grow ml-2 h-6 bg-transparent outline-none placeholder-subtle overflow-hidden whitespace-normal resize-none"
role="textarea"
aria-multiline
placeholder="Find documents based on title / content..."
@ -180,7 +180,7 @@ export function Explorer({
suppressContentEditableWarning={true}
/>
</div>
<div className="mt-4">
<div className="mt-4 border-b border-border">
<HorizontalFilters
{...filterManager}
availableDocumentSets={documentSets}
@ -203,7 +203,7 @@ export function Explorer({
</div>
)}
{!query && (
<div className="flex text-gray-400 mt-3">
<div className="flex text-emphasis mt-3">
Search for a document above to modify it&apos;s boost or hide it from
searches.
</div>

View File

@ -1,6 +1,14 @@
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useState } from "react";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
import { PageSelector } from "@/components/PageSelector";
import { DocumentBoostStatus } from "@/lib/types";
import { updateHiddenStatus } from "../lib";
@ -30,7 +38,7 @@ const IsVisibleSection = ({
);
onUpdate(response);
}}
className="flex text-red-700 cursor-pointer hover:bg-gray-700 py-1 px-2 w-fit rounded-full"
className="flex text-error cursor-pointer hover:bg-hover py-1 px-2 w-fit rounded-full"
>
<div className="select-none">Hidden</div>
<div className="ml-1 my-auto">
@ -46,9 +54,9 @@ const IsVisibleSection = ({
);
onUpdate(response);
}}
className="flex text-gray-400 cursor-pointer hover:bg-gray-700 py-1 px-2 w-fit rounded-full"
className="flex cursor-pointer hover:bg-hover py-1 px-2 w-fit rounded-full"
>
<div className="text-gray-400 my-auto select-none">Visible</div>
<div className="my-auto select-none">Visible</div>
<div className="ml-1 my-auto">
<CustomCheckbox checked={true} />
</div>
@ -56,7 +64,7 @@ const IsVisibleSection = ({
)
}
popupContent={
<div className="text-xs text-gray-300">
<div className="text-xs">
{document.hidden ? (
<div className="flex">
<FiEye className="my-auto mr-1" /> Unhide
@ -86,28 +94,21 @@ export const DocumentFeedbackTable = ({
return (
<div>
{popup}
<BasicTable
columns={[
{
header: "Document Name",
key: "name",
},
{
header: "Is Searchable?",
key: "visible",
},
{
header: "Score",
key: "score",
alignment: "right",
},
]}
data={documents
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Document Name</TableHeaderCell>
<TableHeaderCell>Is Searchable?</TableHeaderCell>
<TableHeaderCell>Score</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{documents
.slice((page - 1) * numToDisplay, page * numToDisplay)
.map((document) => {
return {
name: (
return (
<TableRow key={document.document_id}>
<TableCell className="whitespace-normal break-all">
<a
className="text-blue-600"
href={document.link}
@ -116,8 +117,8 @@ export const DocumentFeedbackTable = ({
>
{document.semantic_id}
</a>
),
visible: (
</TableCell>
<TableCell>
<IsVisibleSection
document={document}
onUpdate={async (response) => {
@ -133,10 +134,13 @@ export const DocumentFeedbackTable = ({
}
}}
/>
),
score: (
</TableCell>
<TableCell>
<div className="ml-auto flex w-16">
<div key={document.document_id} className="h-10 ml-auto mr-8">
<div
key={document.document_id}
className="h-10 ml-auto mr-8"
>
<ScoreSection
documentId={document.document_id}
initialScore={document.boost}
@ -145,10 +149,13 @@ export const DocumentFeedbackTable = ({
/>
</div>
</div>
),
};
</TableCell>
</TableRow>
);
})}
/>
</TableBody>
</Table>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector

View File

@ -6,6 +6,7 @@ import { useMostReactedToDocuments } from "@/lib/hooks";
import { DocumentFeedbackTable } from "./DocumentFeedbackTable";
import { numPages, numToDisplay } from "./constants";
import { AdminPageTitle } from "@/components/admin/Title";
import { Title } from "@tremor/react";
const Main = () => {
const {
@ -47,10 +48,10 @@ const Main = () => {
return (
<div className="mb-8">
<h2 className="font-bold text-xl mb-2">Most Liked Documents</h2>
<Title className="mb-2">Most Liked Documents</Title>
<DocumentFeedbackTable documents={mostLikedDocuments} refresh={refresh} />
<h2 className="font-bold text-xl mb-2 mt-4">Most Disliked Documents</h2>
<Title className="mb-2 mt-6">Most Disliked Documents</Title>
<DocumentFeedbackTable
documents={mostDislikedDocuments}
refresh={refresh}
@ -61,7 +62,7 @@ const Main = () => {
const Page = () => {
return (
<div>
<div className="container mx-auto">
<AdminPageTitle
icon={<ThumbsUpIcon size={32} />}
title="Document Feedback"

View File

@ -5,6 +5,7 @@ import { createDocumentSet, updateDocumentSet } from "./lib";
import { ConnectorIndexingStatus, DocumentSet } from "@/lib/types";
import { TextFormField } from "@/components/admin/connectors/Field";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { Button } from "@tremor/react";
interface SetCreationPopupProps {
ccPairs: ConnectorIndexingStatus<any, any>[];
@ -28,7 +29,7 @@ export const DocumentSetCreationForm = ({
onClick={onClose}
>
<div
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
className="bg-background p-6 rounded border border-border shadow-lg relative w-1/2 text-sm"
onClick={(event) => event.stopPropagation()}
>
<Formik
@ -87,7 +88,7 @@ export const DocumentSetCreationForm = ({
>
{({ isSubmitting, values }) => (
<Form>
<h2 className="text-lg font-bold mb-3">
<h2 className="text-lg text-emphasis font-bold mb-3">
{isUpdate
? "Update a Document Set"
: "Create a new Document Set"}
@ -105,7 +106,9 @@ export const DocumentSetCreationForm = ({
placeholder="Describe what the document set represents"
autoCompleteDisabled={true}
/>
<h2 className="mb-1">Pick your connectors:</h2>
<h2 className="mb-1 font-medium text-base">
Pick your connectors:
</h2>
<p className="mb-3 text-xs">
All documents indexed by the selected connectors will be a
part of this document set.
@ -126,13 +129,13 @@ export const DocumentSetCreationForm = ({
py-1
rounded-lg
border
border-gray-700
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-gray-600"
: " hover:bg-gray-700")
? " bg-background-strong"
: " hover:bg-hover")
}
onClick={() => {
if (isSelected) {
@ -158,17 +161,13 @@ export const DocumentSetCreationForm = ({
)}
/>
<div className="flex">
<button
<Button
type="submit"
disabled={isSubmitting}
className={
"bg-slate-500 hover:bg-slate-700 text-white " +
"font-bold py-2 px-4 rounded focus:outline-none " +
"focus:shadow-outline w-full max-w-sm mx-auto"
}
className="w-64 mx-auto"
>
{isUpdate ? "Update!" : "Create!"}
</button>
</Button>
</div>
</Form>
)}

View File

@ -1,6 +1,5 @@
"use client";
import { Button } from "@/components/Button";
import { LoadingAnimation, ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
@ -10,6 +9,17 @@ import {
InfoIcon,
TrashIcon,
} from "@/components/icons/icons";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Title,
Divider,
Badge,
} from "@tremor/react";
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
import { ConnectorIndexingStatus, DocumentSet } from "@/lib/types";
import { useState } from "react";
@ -19,7 +29,14 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { deleteDocumentSet } from "./lib";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { AdminPageTitle } from "@/components/admin/Title";
import { Text } from "@tremor/react";
import { Button, Text } from "@tremor/react";
import {
FiAlertTriangle,
FiCheckCircle,
FiClock,
FiEdit,
} from "react-icons/fi";
import { DeleteButton } from "@/components/DeleteButton";
const numToDisplay = 50;
@ -50,15 +67,15 @@ const EditRow = ({
/>
)}
{isSyncingTooltipOpen && (
<div className="flex flex-nowrap absolute w-64 top-0 left-0 mt-8 bg-gray-700 px-3 py-2 rounded shadow-lg">
<InfoIcon className="mt-1 flex flex-shrink-0 mr-2 text-gray-300" />{" "}
Cannot update while syncing! Wait for the sync to finish, then try
again.
<div className="flex flex-nowrap absolute w-64 top-0 left-0 mt-8 border border-border bg-background px-3 py-2 rounded shadow-lg">
<InfoIcon className="mt-1 flex flex-shrink-0 mr-2" /> Cannot update
while syncing! Wait for the sync to finish, then try again.
</div>
)}
<div
className={
"my-auto" + (documentSet.is_up_to_date ? " cursor-pointer" : "")
"text-emphasis font-medium my-auto p-1 hover:bg-hover-light flex" +
(documentSet.is_up_to_date ? " cursor-pointer" : "")
}
onClick={() => {
if (documentSet.is_up_to_date) {
@ -76,7 +93,8 @@ const EditRow = ({
}
}}
>
<EditIcon />
<FiEdit className="text-emphasis mr-1 my-auto" />
{documentSet.name}
</div>
</div>
);
@ -110,49 +128,41 @@ const DocumentSetTable = ({
return (
<div>
<BasicTable
columns={[
{
header: "Name",
key: "name",
},
{
header: "Connectors",
key: "ccPairs",
},
{
header: "Status",
key: "status",
},
{
header: "Delete",
key: "delete",
width: "50px",
},
]}
data={documentSets
<Title>Existing Document Sets</Title>
<Table className="overflow-visible mt-2">
<TableHead>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Connectors</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Delete</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{documentSets
.slice((page - 1) * numToDisplay, page * numToDisplay)
.map((documentSet) => {
return {
name: (
<div className="flex gap-x-2">
return (
<TableRow key={documentSet.id}>
<TableCell className="whitespace-normal break-all">
<div className="flex gap-x-1 text-emphasis">
<EditRow
documentSet={documentSet}
ccPairs={ccPairs}
setPopup={setPopup}
refreshDocumentSets={refresh}
/>{" "}
<b className="my-auto">{documentSet.name}</b>
/>
</div>
),
ccPairs: (
</TableCell>
<TableCell>
<div>
{documentSet.cc_pair_descriptors.map(
(ccPairDescriptor, ind) => {
return (
<div
className={
ind !== documentSet.cc_pair_descriptors.length - 1
ind !==
documentSet.cc_pair_descriptors.length - 1
? "mb-3"
: ""
}
@ -169,23 +179,28 @@ const DocumentSetTable = ({
}
)}
</div>
),
status: documentSet.is_up_to_date ? (
<div className="text-emerald-600">Up to date!</div>
</TableCell>
<TableCell>
{documentSet.is_up_to_date ? (
<Badge size="md" color="green" icon={FiCheckCircle}>
Up to Date
</Badge>
) : documentSet.cc_pair_descriptors.length > 0 ? (
<div className="text-gray-300 w-10">
<LoadingAnimation text="Syncing" />
</div>
<Badge size="md" color="amber" icon={FiClock}>
Syncing
</Badge>
) : (
<div className="text-red-500 w-10">
<LoadingAnimation text="Deleting" />
</div>
),
delete: (
<div
className="cursor-pointer"
<Badge size="md" color="red" icon={FiAlertTriangle}>
Deleting
</Badge>
)}
</TableCell>
<TableCell>
<DeleteButton
onClick={async () => {
const response = await deleteDocumentSet(documentSet.id);
const response = await deleteDocumentSet(
documentSet.id
);
if (response.ok) {
setPopup({
message: `Document set "${documentSet.name}" scheduled for deletion`,
@ -200,13 +215,14 @@ const DocumentSetTable = ({
}
refresh();
}}
>
<TrashIcon />
</div>
),
};
})}
/>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
@ -251,7 +267,7 @@ const Main = () => {
return (
<div className="mb-8">
{popup}
<Text className="mb-3 text-gray-300">
<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
@ -259,21 +275,29 @@ const Main = () => {
or with a certain command.
</Text>
<div className="mb-6"></div>
<div className="mb-3"></div>
<div className="flex mb-3">
<Button className="ml-2 my-auto" onClick={() => setIsOpen(true)}>
<div className="flex mb-6">
<Button
size="xs"
color="green"
className="ml-2 my-auto"
onClick={() => setIsOpen(true)}
>
New Document Set
</Button>
</div>
{documentSets.length > 0 && (
<>
<Divider />
<DocumentSetTable
documentSets={documentSets}
ccPairs={ccPairs}
refresh={refreshDocumentSets}
setPopup={setPopup}
/>
</>
)}
{isOpen && (
@ -292,7 +316,7 @@ const Main = () => {
const Page = () => {
return (
<div>
<div className="container mx-auto">
<AdminPageTitle icon={<BookmarkIcon size={32} />} title="Document Sets" />
<Main />

View File

@ -80,7 +80,7 @@ export function CCPairIndexingStatusTable({
);
return (
<div className="dark">
<div>
<Table className="overflow-visible">
<TableHead>
<TableRow>
@ -96,12 +96,12 @@ export function CCPairIndexingStatusTable({
<TableRow
key={ccPairsIndexingStatus.cc_pair_id}
className={
"hover:bg-gradient-to-r hover:from-gray-800 hover:to-indigo-950 cursor-pointer relative"
"hover:bg-hover-light bg-background cursor-pointer relative"
}
>
<TableCell>
<div className="flex my-auto">
<FiEdit className="mr-4 my-auto text-blue-300" />
<FiEdit className="mr-4 my-auto" />
<div className="whitespace-normal break-all max-w-3xl">
<ConnectorTitle
connector={ccPairsIndexingStatus.connector}

View File

@ -60,13 +60,13 @@ function Main() {
export default function Status() {
return (
<div className="mx-auto container dark">
<div className="mx-auto container">
<AdminPageTitle
icon={<NotebookIcon size={32} />}
title="Existing Connectors"
farRightElement={
<Link href="/admin/add-connector">
<Button variant="secondary" size="xs">
<Button color="green" size="xs">
Add Connector
</Button>
</Link>

View File

@ -6,6 +6,7 @@ import { KeyIcon, TrashIcon } from "@/components/icons/icons";
import { ApiKeyForm } from "@/components/openai/ApiKeyForm";
import { GEN_AI_API_KEY_URL } from "@/components/openai/constants";
import { fetcher } from "@/lib/fetcher";
import { Text, Title } from "@tremor/react";
import useSWR, { mutate } from "swr";
const ExistingKeys = () => {
@ -19,7 +20,7 @@ const ExistingKeys = () => {
}
if (error) {
return <div className="text-red-600">Error loading existing keys</div>;
return <div className="text-error">Error loading existing keys</div>;
}
if (!data?.api_key) {
@ -28,11 +29,11 @@ const ExistingKeys = () => {
return (
<div>
<h2 className="text-lg font-bold mb-2">Existing Key</h2>
<Title className="mb-2">Existing Key</Title>
<div className="flex mb-1">
<p className="text-sm italic my-auto">sk- ****...**{data?.api_key}</p>
<button
className="ml-1 my-auto hover:bg-gray-700 rounded-full p-1"
className="ml-1 my-auto hover:bg-hover rounded p-1"
onClick={async () => {
await fetch(GEN_AI_API_KEY_URL, {
method: "DELETE",
@ -49,16 +50,16 @@ const ExistingKeys = () => {
const Page = () => {
return (
<div>
<div className="mx-auto container">
<AdminPageTitle title="OpenAI Keys" icon={<KeyIcon size={32} />} />
<ExistingKeys />
<h2 className="text-lg font-bold mb-2">Update Key</h2>
<p className="text-sm mb-2">
<Title className="mb-2 mt-6">Update Key</Title>
<Text className="mb-2">
Specify an OpenAI API key and click the &quot;Submit&quot; button.
</p>
<div className="border rounded-md border-gray-700 p-3">
</Text>
<div className="border rounded-md border-border p-3">
<ApiKeyForm
handleResponse={(response) => {
if (response.ok) {

View File

@ -2,7 +2,7 @@
import { DocumentSet } from "@/lib/types";
import { Button, Divider, Text } from "@tremor/react";
import { ArrayHelpers, ErrorMessage, FieldArray, Form, Formik } from "formik";
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
import * as Yup from "yup";
import { buildFinalPrompt, createPersona, updatePersona } from "./lib";
@ -13,7 +13,6 @@ import Link from "next/link";
import { useEffect, useState } from "react";
import {
BooleanFormField,
ManualErrorMessage,
SelectorFormField,
TextFormField,
} from "@/components/admin/connectors/Field";
@ -24,12 +23,12 @@ function SectionHeader({ children }: { children: string | JSX.Element }) {
function Label({ children }: { children: string | JSX.Element }) {
return (
<div className="block font-medium text-base text-gray-200">{children}</div>
<div className="block font-medium text-base text-emphasis">{children}</div>
);
}
function SubLabel({ children }: { children: string | JSX.Element }) {
return <div className="text-sm text-gray-300 mb-2">{children}</div>;
return <div className="text-sm text-subtle mb-2">{children}</div>;
}
export function PersonaEditor({
@ -65,35 +64,35 @@ export function PersonaEditor({
};
const isUpdate = existingPersona !== undefined && existingPersona !== null;
const existingPrompt = existingPersona?.prompts[0] ?? null;
useEffect(() => {
if (isUpdate) {
triggerFinalPromptUpdate(
existingPersona.system_prompt,
existingPersona.task_prompt,
existingPrompt!.system_prompt,
existingPrompt!.task_prompt,
existingPersona.num_chunks === 0
);
}
}, []);
return (
<div className="dark">
<div>
{popup}
<Formik
enableReinitialize={true}
initialValues={{
name: existingPersona?.name ?? "",
description: existingPersona?.description ?? "",
system_prompt: existingPersona?.system_prompt ?? "",
task_prompt: existingPersona?.task_prompt ?? "",
system_prompt: existingPrompt?.system_prompt ?? "",
task_prompt: existingPrompt?.task_prompt ?? "",
disable_retrieval: (existingPersona?.num_chunks ?? 5) === 0,
document_set_ids:
existingPersona?.document_sets?.map(
(documentSet) => documentSet.id
) ?? ([] as number[]),
num_chunks: existingPersona?.num_chunks ?? null,
apply_llm_relevance_filter:
existingPersona?.apply_llm_relevance_filter ?? false,
llm_relevance_filter: existingPersona?.llm_relevance_filter ?? false,
llm_model_version_override:
existingPersona?.llm_model_version_override ?? null,
}}
@ -108,7 +107,7 @@ export function PersonaEditor({
disable_retrieval: Yup.boolean().required(),
document_set_ids: Yup.array().of(Yup.number()),
num_chunks: Yup.number().max(20).nullable(),
apply_llm_relevance_filter: Yup.boolean().required(),
llm_relevance_filter: Yup.boolean().required(),
llm_model_version_override: Yup.string().nullable(),
})
.test(
@ -148,29 +147,39 @@ export function PersonaEditor({
? 0
: values.num_chunks || 5;
let response;
let promptResponse;
let personaResponse;
if (isUpdate) {
response = await updatePersona({
[promptResponse, personaResponse] = await updatePersona({
id: existingPersona.id,
existingPromptId: existingPrompt!.id,
...values,
num_chunks: numChunks,
});
} else {
response = await createPersona({
[promptResponse, personaResponse] = await createPersona({
...values,
num_chunks: numChunks,
});
}
if (response.ok) {
router.push(`/admin/personas?u=${Date.now()}`);
return;
let error = null;
if (!promptResponse.ok) {
error = await promptResponse.text();
}
if (personaResponse && !personaResponse.ok) {
error = await personaResponse.text();
}
if (error) {
setPopup({
type: "error",
message: `Failed to create Persona - ${await response.text()}`,
message: `Failed to create Persona - ${error}`,
});
formikHelpers.setSubmitting(false);
} else {
router.push(`/admin/personas?u=${Date.now()}`);
}
}}
>
{({ isSubmitting, values, setFieldValue }) => (
@ -303,13 +312,13 @@ export function PersonaEditor({
py-1
rounded-lg
border
border-gray-700
border-border
w-fit
flex
cursor-pointer ` +
(isSelected
? " bg-gray-600"
: " bg-gray-900 hover:bg-gray-700")
? " bg-hover"
: " bg-background hover:bg-hover-light")
}
onClick={() => {
if (isSelected) {
@ -354,6 +363,7 @@ export function PersonaEditor({
.
</Text>
<div className="w-96">
<SelectorFormField
name="llm_model_version_override"
options={llmOverrideOptions.map((llmOption) => {
@ -364,6 +374,7 @@ export function PersonaEditor({
})}
includeDefault={true}
/>
</div>
</>
)}
@ -401,7 +412,7 @@ export function PersonaEditor({
/>
<BooleanFormField
name="apply_llm_relevance_filter"
name="llm_relevance_filter"
label="Apply LLM Relevance Filter"
subtext={
"If enabled, the LLM will filter out chunks that are not relevant to the user query."
@ -415,7 +426,7 @@ export function PersonaEditor({
<div className="flex">
<Button
className="mx-auto"
variant="secondary"
color="green"
size="md"
type="submit"
disabled={isSubmitting}

View File

@ -11,20 +11,22 @@ import {
import { Persona } from "./interfaces";
import { EditButton } from "@/components/EditButton";
import { useRouter } from "next/navigation";
import { FiInfo } from "react-icons/fi";
export function PersonasTable({ personas }: { personas: Persona[] }) {
const router = useRouter();
const sortedPersonas = [...personas];
sortedPersonas.sort((a, b) => a.name.localeCompare(b.name));
sortedPersonas.sort((a, b) => (a.id > b.id ? 1 : -1));
return (
<div className="dark">
<div>
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Name</TableHeaderCell>
<TableHeaderCell>Description</TableHeaderCell>
<TableHeaderCell>Built-In</TableHeaderCell>
<TableHeaderCell></TableHeaderCell>
</TableRow>
</TableHead>
@ -36,10 +38,21 @@ export function PersonasTable({ personas }: { personas: Persona[] }) {
<p className="text font-medium">{persona.name}</p>
</TableCell>
<TableCell>{persona.description}</TableCell>
<TableCell>{persona.default_persona ? "Yes" : "No"}</TableCell>
<TableCell>
<div className="flex">
<div className="mx-auto">
{!persona.default_persona ? (
<EditButton
onClick={() => router.push(`/admin/personas/${persona.id}`)}
onClick={() =>
router.push(`/admin/personas/${persona.id}`)
}
/>
) : (
"-"
)}
</div>
</div>
</TableCell>
</TableRow>
);

View File

@ -10,7 +10,6 @@ export function DeletePersonaButton({ personaId }: { personaId: number }) {
return (
<Button
variant="secondary"
size="xs"
color="red"
onClick={async () => {

View File

@ -67,7 +67,7 @@ export default async function Page({
const defaultLLM = (await defaultLLMResponse.json()) as string;
return (
<div className="dark">
<div>
<InstantSSRAutoRefresh />
<BackButton />

View File

@ -1,13 +1,27 @@
import { DocumentSet } from "@/lib/types";
export interface Prompt {
id: number;
name: string;
shared: boolean;
description: string;
system_prompt: string;
task_prompt: string;
include_citations: boolean;
datetime_aware: boolean;
default_prompt: boolean;
}
export interface Persona {
id: number;
name: string;
shared: boolean;
description: string;
system_prompt: string;
task_prompt: string;
document_sets: DocumentSet[];
prompts: Prompt[];
num_chunks?: number;
apply_llm_relevance_filter?: boolean;
llm_relevance_filter?: boolean;
llm_filter_extraction?: boolean;
llm_model_version_override?: string;
default_persona: boolean;
}

View File

@ -1,3 +1,5 @@
import { Prompt } from "./interfaces";
interface PersonaCreationRequest {
name: string;
description: string;
@ -5,41 +7,157 @@ interface PersonaCreationRequest {
task_prompt: string;
document_set_ids: number[];
num_chunks: number | null;
apply_llm_relevance_filter: boolean | null;
}
export function createPersona(personaCreationRequest: PersonaCreationRequest) {
return fetch("/api/admin/persona", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(personaCreationRequest),
});
llm_relevance_filter: boolean | null;
}
interface PersonaUpdateRequest {
id: number;
existingPromptId: number;
name: string;
description: string;
system_prompt: string;
task_prompt: string;
document_set_ids: number[];
num_chunks: number | null;
apply_llm_relevance_filter: boolean | null;
llm_relevance_filter: boolean | null;
}
export function updatePersona(personaUpdateRequest: PersonaUpdateRequest) {
const { id, ...requestBody } = personaUpdateRequest;
function promptNameFromPersonaName(personaName: string) {
return `default-prompt__${personaName}`;
}
return fetch(`/api/admin/persona/${id}`, {
function createPrompt({
personaName,
systemPrompt,
taskPrompt,
}: {
personaName: string;
systemPrompt: string;
taskPrompt: string;
}) {
return fetch("/api/prompt", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
name: promptNameFromPersonaName(personaName),
description: `Default prompt for persona ${personaName}`,
shared: true,
system_prompt: systemPrompt,
task_prompt: taskPrompt,
}),
});
}
function updatePrompt({
promptId,
personaName,
systemPrompt,
taskPrompt,
}: {
promptId: number;
personaName: string;
systemPrompt: string;
taskPrompt: string;
}) {
return fetch(`/api/prompt/${promptId}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(requestBody),
body: JSON.stringify({
name: promptNameFromPersonaName(personaName),
description: `Default prompt for persona ${personaName}`,
shared: true,
system_prompt: systemPrompt,
task_prompt: taskPrompt,
}),
});
}
function buildPersonaAPIBody(
creationRequest: PersonaCreationRequest | PersonaUpdateRequest,
promptId: number
) {
const {
name,
description,
document_set_ids,
num_chunks,
llm_relevance_filter,
} = creationRequest;
return {
name,
description,
shared: true,
num_chunks,
llm_relevance_filter,
llm_filter_extraction: false,
recency_bias: "base_decay",
prompt_ids: [promptId],
document_set_ids,
};
}
export async function createPersona(
personaCreationRequest: PersonaCreationRequest
): Promise<[Response, Response | null]> {
// first create prompt
const createPromptResponse = await createPrompt({
personaName: personaCreationRequest.name,
systemPrompt: personaCreationRequest.system_prompt,
taskPrompt: personaCreationRequest.task_prompt,
});
const promptId = createPromptResponse.ok
? (await createPromptResponse.json()).id
: null;
const createPersonaResponse =
promptId !== null
? await fetch("/api/admin/persona", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
buildPersonaAPIBody(personaCreationRequest, promptId)
),
})
: null;
return [createPromptResponse, createPersonaResponse];
}
export async function updatePersona(
personaUpdateRequest: PersonaUpdateRequest
): Promise<[Response, Response | null]> {
const { id, existingPromptId, ...requestBody } = personaUpdateRequest;
// first update prompt
const updatePromptResponse = await updatePrompt({
promptId: existingPromptId,
personaName: personaUpdateRequest.name,
systemPrompt: personaUpdateRequest.system_prompt,
taskPrompt: personaUpdateRequest.task_prompt,
});
const updatePersonaResponse = updatePromptResponse.ok
? await fetch(`/api/admin/persona/${id}`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
buildPersonaAPIBody(personaUpdateRequest, existingPromptId)
),
})
: null;
return [updatePromptResponse, updatePersonaResponse];
}
export function deletePersona(personaId: number) {
return fetch(`/api/admin/persona/${personaId}`, {
method: "DELETE",

View File

@ -46,7 +46,7 @@ export default async function Page() {
const defaultLLM = (await defaultLLMResponse.json()) as string;
return (
<div className="dark">
<div>
<BackButton />
<AdminPageTitle

View File

@ -23,7 +23,7 @@ export default async function Page() {
const personas = (await personaResponse.json()) as Persona[];
return (
<div className="dark">
<div className="mx-auto container">
<AdminPageTitle icon={<RobotIcon size={32} />} title="Personas" />
<Text className="mb-2">
@ -31,7 +31,7 @@ export default async function Page() {
for different use cases.
</Text>
<Text className="mt-2">They allow you to customize:</Text>
<div className="text-dark-tremor-content text-sm">
<div className="text-sm">
<ul className="list-disc mt-2 ml-4">
<li>
The prompt used by your LLM of choice to respond to the user query
@ -40,13 +40,13 @@ export default async function Page() {
</ul>
</div>
<div className="dark">
<div>
<Divider />
<Title>Create a Persona</Title>
<Link
href="/admin/personas/new"
className="text-gray-100 flex py-2 px-4 mt-2 border border-gray-800 h-fit cursor-pointer hover:bg-gray-800 text-sm w-36"
className="flex py-2 px-4 mt-2 border border-border h-fit cursor-pointer hover:bg-hover text-sm w-36"
>
<div className="mx-auto flex">
<FiPlusSquare className="my-auto mr-2" />

View File

@ -1,57 +1,66 @@
"use client";
import { Button } from "@/components/Button";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Button,
} from "@tremor/react";
import { LoadingAnimation } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { UsersIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher";
import { User } from "@/lib/types";
import useSWR, { mutate } from "swr";
const columns = [
{
header: "Email",
key: "email",
},
{
header: "Role",
key: "role",
},
{
header: "Promote",
key: "promote",
},
];
const UsersTable = () => {
const { popup, setPopup } = usePopup();
const { data, isLoading, error } = useSWR<User[]>(
"/api/manage/users",
fetcher
);
const {
data: users,
isLoading,
error,
} = useSWR<User[]>("/api/manage/users", fetcher);
if (isLoading) {
return <LoadingAnimation text="Loading" />;
}
if (error || !data) {
return <div className="text-red-600">Error loading users</div>;
if (error || !users) {
return <div className="text-error">Error loading users</div>;
}
return (
<div>
{popup}
<BasicTable
columns={columns}
data={data.map((user) => {
return {
email: user.email,
role: <i>{user.role === "admin" ? "Admin" : "User"}</i>,
promote:
user.role !== "admin" ? (
<Table className="overflow-visible">
<TableHead>
<TableRow>
<TableHeaderCell>Email</TableHeaderCell>
<TableHeaderCell>Role</TableHeaderCell>
<TableHeaderCell>
<div className="flex">
<div className="ml-auto">Promote</div>
</div>
</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{users.map((user) => {
return (
<TableRow key={user.id}>
<TableCell>{user.email}</TableCell>
<TableCell>
<i>{user.role === "admin" ? "Admin" : "User"}</i>
</TableCell>
<TableCell>
<div className="flex">
<div className="ml-auto">
<Button
onClick={async () => {
const res = await fetch(
@ -83,19 +92,21 @@ const UsersTable = () => {
>
Promote to Admin!
</Button>
) : (
""
),
};
</div>
</div>
</TableCell>
</TableRow>
);
})}
/>
</TableBody>
</Table>
</div>
);
};
const Page = () => {
return (
<div>
<div className="mx-auto container">
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
<UsersTable />

View File

@ -42,7 +42,7 @@ export function SignInButton({
return (
<a
className="mt-6 py-3 w-72 bg-blue-900 flex rounded cursor-pointer hover:bg-blue-950"
className="mt-6 py-3 w-72 text-gray-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
href={authorizeUrl}
>
{button}

View File

@ -77,7 +77,7 @@ const Page = async ({
<div className="h-16 w-16 mx-auto">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
<h2 className="text-center text-xl font-bold mt-4">
<h2 className="text-center text-xl text-strong font-bold mt-6">
Log In to Danswer
</h2>
{authUrl && authTypeMetadata && (

584
web/src/app/chat/Chat.tsx Normal file
View File

@ -0,0 +1,584 @@
"use client";
import Image from "next/image";
import { useEffect, useRef, useState } from "react";
import { FiRefreshCcw, FiSend, FiStopCircle } from "react-icons/fi";
import { AIMessage, HumanMessage } from "./message/Messages";
import { AnswerPiecePacket, DanswerDocument } from "@/lib/search/interfaces";
import {
BackendMessage,
DocumentsResponse,
Message,
RetrievalType,
StreamingError,
} from "./interfaces";
import { useRouter } from "next/navigation";
import { FeedbackType } from "./types";
import {
createChatSession,
getCitedDocumentsFromMessage,
getHumanAndAIMessageFromMessageNumber,
handleAutoScroll,
handleChatFeedback,
nameChatSession,
sendMessage,
} from "./lib";
import { ThreeDots } from "react-loader-spinner";
import { FeedbackModal } from "./modal/FeedbackModal";
import { DocumentSidebar } from "./documentSidebar/DocumentSidebar";
import { Persona } from "../admin/personas/interfaces";
import { ChatPersonaSelector } from "./ChatPersonaSelector";
import { useFilters } from "@/lib/hooks";
import { DocumentSet, ValidSources } from "@/lib/types";
import { ChatFilters } from "./modifiers/ChatFilters";
import { buildFilters } from "@/lib/search/utils";
import { QA, SearchTypeSelector } from "./modifiers/SearchTypeSelector";
import { SelectedDocuments } from "./modifiers/SelectedDocuments";
import { usePopup } from "@/components/admin/connectors/Popup";
import { useSWRConfig } from "swr";
const MAX_INPUT_HEIGHT = 200;
export const Chat = ({
existingChatSessionId,
existingChatSessionPersonaId,
existingMessages,
availableSources,
availableDocumentSets,
availablePersonas,
shouldhideBeforeScroll,
}: {
existingChatSessionId: number | null;
existingChatSessionPersonaId: number | undefined;
existingMessages: Message[];
availableSources: ValidSources[];
availableDocumentSets: DocumentSet[];
availablePersonas: Persona[];
shouldhideBeforeScroll?: boolean;
}) => {
const router = useRouter();
const { popup, setPopup } = usePopup();
const [chatSessionId, setChatSessionId] = useState(existingChatSessionId);
const [message, setMessage] = useState("");
const [messageHistory, setMessageHistory] =
useState<Message[]>(existingMessages);
const [isStreaming, setIsStreaming] = useState(false);
// for document display
// NOTE: -1 is a special designation that means the latest AI message
const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] =
useState<number | null>(
messageHistory[messageHistory.length - 1]?.messageId || null
);
const { aiMessage } = selectedMessageForDocDisplay
? getHumanAndAIMessageFromMessageNumber(
messageHistory,
selectedMessageForDocDisplay
)
: { aiMessage: null };
const [selectedDocuments, setSelectedDocuments] = useState<DanswerDocument[]>(
[]
);
const [selectedPersona, setSelectedPersona] = useState<Persona | undefined>(
existingChatSessionPersonaId !== undefined
? availablePersonas.find(
(persona) => persona.id === existingChatSessionPersonaId
)
: availablePersonas.find((persona) => persona.name === "Default")
);
const filterManager = useFilters();
const [selectedSearchType, setSelectedSearchType] = useState(QA);
// state for cancelling streaming
const [isCancelled, setIsCancelled] = useState(false);
const isCancelledRef = useRef(isCancelled);
useEffect(() => {
isCancelledRef.current = isCancelled;
}, [isCancelled]);
const [currentFeedback, setCurrentFeedback] = useState<
[FeedbackType, number] | null
>(null);
// auto scroll as message comes out
const scrollableDivRef = useRef<HTMLDivElement>(null);
const endDivRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (isStreaming || !message) {
handleAutoScroll(endDivRef, scrollableDivRef);
}
});
// scroll to bottom initially
console.log(shouldhideBeforeScroll);
const [hasPerformedInitialScroll, setHasPerformedInitialScroll] = useState(
shouldhideBeforeScroll !== true
);
useEffect(() => {
endDivRef.current?.scrollIntoView();
setHasPerformedInitialScroll(true);
}, []);
// handle refreshes of the server-side props
useEffect(() => {
setMessageHistory(existingMessages);
}, [existingMessages]);
// handle re-sizing of the text area
const textareaRef = useRef<HTMLTextAreaElement>(null);
useEffect(() => {
const textarea = textareaRef.current;
if (textarea) {
textarea.style.height = "0px";
textarea.style.height = `${Math.min(
textarea.scrollHeight,
MAX_INPUT_HEIGHT
)}px`;
}
}, [message]);
const onSubmit = async (messageOverride?: string) => {
let currChatSessionId: number;
let isNewSession = chatSessionId === null;
if (isNewSession) {
currChatSessionId = await createChatSession(selectedPersona?.id || 0);
} else {
currChatSessionId = chatSessionId as number;
}
setChatSessionId(currChatSessionId);
const currMessage = messageOverride || message;
const currMessageHistory = messageHistory;
setMessageHistory([
...currMessageHistory,
{
messageId: 0,
message: currMessage,
type: "user",
},
]);
setMessage("");
setIsStreaming(true);
let answer = "";
let query: string | null = null;
let retrievalType: RetrievalType =
selectedDocuments.length > 0
? RetrievalType.SelectedDocs
: RetrievalType.None;
let documents: DanswerDocument[] = selectedDocuments;
let error: string | null = null;
let finalMessage: BackendMessage | null = null;
try {
for await (const packetBunch of sendMessage({
message: currMessage,
parentMessageId:
currMessageHistory.length > 0
? currMessageHistory[currMessageHistory.length - 1].messageId
: null,
chatSessionId: currChatSessionId,
// if search-only set prompt to null to tell backend to not give an answer
promptId:
selectedSearchType === QA ? selectedPersona?.prompts[0]?.id : null,
filters: buildFilters(
filterManager.selectedSources,
filterManager.selectedDocumentSets,
filterManager.timeRange
),
selectedDocumentIds: selectedDocuments
.filter(
(document) =>
document.db_doc_id !== undefined && document.db_doc_id !== null
)
.map((document) => document.db_doc_id as number),
})) {
for (const packet of packetBunch) {
if (Object.hasOwn(packet, "answer_piece")) {
answer += (packet as AnswerPiecePacket).answer_piece;
} else if (Object.hasOwn(packet, "top_documents")) {
documents = (packet as DocumentsResponse).top_documents;
query = (packet as DocumentsResponse).rephrased_query;
retrievalType = RetrievalType.Search;
if (documents && documents.length > 0) {
// point to the latest message (we don't know the messageId yet, which is why
// we have to use -1)
setSelectedMessageForDocDisplay(-1);
}
} else if (Object.hasOwn(packet, "error")) {
error = (packet as StreamingError).error;
} else if (Object.hasOwn(packet, "message_id")) {
finalMessage = packet as BackendMessage;
}
}
setMessageHistory([
...currMessageHistory,
{
messageId: finalMessage?.parent_message || null,
message: currMessage,
type: "user",
},
{
messageId: finalMessage?.message_id || null,
message: error || answer,
type: error ? "error" : "assistant",
retrievalType,
query: finalMessage?.rephrased_query || query,
documents: finalMessage?.context_docs?.top_documents || documents,
citations: finalMessage?.citations || {},
},
]);
if (isCancelledRef.current) {
setIsCancelled(false);
break;
}
}
} catch (e: any) {
const errorMsg = e.message;
setMessageHistory([
...currMessageHistory,
{
messageId: null,
message: currMessage,
type: "user",
},
{
messageId: null,
message: errorMsg,
type: "error",
},
]);
}
setIsStreaming(false);
if (isNewSession) {
if (finalMessage) {
setSelectedMessageForDocDisplay(finalMessage.message_id);
}
await nameChatSession(currChatSessionId, currMessage);
router.push(`/chat/${currChatSessionId}?shouldhideBeforeScroll=true`, {
scroll: false,
});
}
if (
finalMessage?.context_docs &&
finalMessage.context_docs.top_documents.length > 0 &&
retrievalType === RetrievalType.Search
) {
setSelectedMessageForDocDisplay(finalMessage.message_id);
}
};
const onFeedback = async (
messageId: number,
feedbackType: FeedbackType,
feedbackDetails: string
) => {
if (chatSessionId === null) {
return;
}
const response = await handleChatFeedback(
messageId,
feedbackType,
feedbackDetails
);
if (response.ok) {
setPopup({
message: "Thanks for your feedback!",
type: "success",
});
} else {
const responseJson = await response.json();
const errorMsg = responseJson.detail || responseJson.message;
setPopup({
message: `Failed to submit feedback - ${errorMsg}`,
type: "error",
});
}
};
return (
<div className="flex w-full overflow-x-hidden">
{popup}
{currentFeedback && (
<FeedbackModal
feedbackType={currentFeedback[0]}
onClose={() => setCurrentFeedback(null)}
onSubmit={(feedbackDetails) => {
onFeedback(currentFeedback[1], currentFeedback[0], feedbackDetails);
setCurrentFeedback(null);
}}
/>
)}
<div className="w-full sm:relative">
<div
className="w-full h-screen flex flex-col overflow-y-auto relative"
ref={scrollableDivRef}
>
{selectedPersona && (
<div className="sticky top-0 left-80 z-10 w-full bg-background/90">
<div className="ml-2 p-1 rounded mt-2 w-fit">
<ChatPersonaSelector
personas={availablePersonas}
selectedPersonaId={selectedPersona?.id}
onPersonaChange={(persona) => {
if (persona) {
setSelectedPersona(persona);
}
}}
/>
</div>
</div>
)}
{messageHistory.length === 0 && !isStreaming && (
<div className="flex justify-center items-center h-full">
<div>
<div className="flex">
<div className="mx-auto h-[80px] w-[80px]">
<Image
src="/logo.png"
alt="Logo"
width="1419"
height="1520"
/>
</div>
</div>
<div className="text-2xl font-bold text-strong p-4">
What are you looking for today?
</div>
</div>
</div>
)}
<div
className={
"mt-4 pt-12 sm:pt-0 mx-8" +
(hasPerformedInitialScroll ? "" : " invisible")
}
>
{messageHistory.map((message, i) => {
if (message.type === "user") {
return (
<div key={i}>
<HumanMessage content={message.message} />
</div>
);
} else if (message.type === "assistant") {
const isShowingRetrieved =
(selectedMessageForDocDisplay !== null &&
selectedMessageForDocDisplay === message.messageId) ||
(selectedMessageForDocDisplay === -1 &&
i === messageHistory.length - 1);
return (
<div key={i}>
<AIMessage
messageId={message.messageId}
content={message.message}
query={messageHistory[i]?.query || undefined}
citedDocuments={getCitedDocumentsFromMessage(message)}
isComplete={
i !== messageHistory.length - 1 || !isStreaming
}
hasDocs={
(message.documents && message.documents.length > 0) ===
true
}
handleFeedback={
i === messageHistory.length - 1 && isStreaming
? undefined
: (feedbackType) =>
setCurrentFeedback([
feedbackType,
message.messageId as number,
])
}
isCurrentlyShowingRetrieved={isShowingRetrieved}
handleShowRetrieved={(messageNumber) => {
if (isShowingRetrieved) {
setSelectedMessageForDocDisplay(null);
} else {
if (messageNumber !== null) {
setSelectedMessageForDocDisplay(messageNumber);
} else {
setSelectedMessageForDocDisplay(-1);
}
}
}}
/>
</div>
);
} else {
return (
<div key={i}>
<AIMessage
messageId={message.messageId}
content={
<p className="text-red-700 text-sm my-auto">
{message.message}
</p>
}
/>
</div>
);
}
})}
{isStreaming &&
messageHistory.length &&
messageHistory[messageHistory.length - 1].type === "user" && (
<div key={messageHistory.length}>
<AIMessage
messageId={null}
content={
<div className="text-sm my-auto">
<ThreeDots
height="30"
width="50"
color="#3b82f6"
ariaLabel="grid-loading"
radius="12.5"
wrapperStyle={{}}
wrapperClass=""
visible={true}
/>
</div>
}
/>
</div>
)}
{/* Some padding at the bottom so the search bar has space at the bottom to not cover the last message*/}
<div className={`min-h-[200px] w-full`}></div>
<div ref={endDivRef} />
</div>
</div>
<div className="absolute bottom-0 z-10 w-full bg-background border-t border-border">
<div className="w-full pb-4 pt-2">
{/* {(isStreaming || messageHistory.length > 0) && (
<div className="flex justify-center w-full">
<div className="w-[800px] flex">
<div className="cursor-pointer flex w-fit p-2 rounded border border-neutral-400 text-sm hover:bg-neutral-200 ml-auto mr-4">
{isStreaming ? (
<div
onClick={() => setIsCancelled(true)}
className="flex"
>
<FiStopCircle className="my-auto mr-1" />
<div>Stop Generating</div>
</div>
) : (
<div
className="flex"
onClick={() => {
if (chatSessionId) {
handleRegenerate(chatSessionId);
}
}}
>
<FiRefreshCcw className="my-auto mr-1" />
<div>Regenerate</div>
</div>
)}
</div>
</div>
</div>
)} */}
<div className="flex">
<div className="w-searchbar mx-auto px-4 pt-1 flex">
<div className="mr-3">
<SearchTypeSelector
selectedSearchType={selectedSearchType}
setSelectedSearchType={setSelectedSearchType}
/>
</div>
{selectedDocuments.length > 0 ? (
<SelectedDocuments selectedDocuments={selectedDocuments} />
) : (
<ChatFilters
{...filterManager}
existingSources={availableSources}
availableDocumentSets={availableDocumentSets}
/>
)}
</div>
</div>
<div className="flex justify-center py-2 max-w-screen-lg mx-auto mb-2">
<div className="w-full shrink relative px-4 w-searchbar mx-auto">
<textarea
ref={textareaRef}
autoFocus
className={`
opacity-100
w-full
shrink
border
border-border
rounded-lg
outline-none
placeholder-gray-400
pl-4
pr-12
py-4
overflow-hidden
h-14
${
(textareaRef?.current?.scrollHeight || 0) >
MAX_INPUT_HEIGHT
? "overflow-y-auto"
: ""
}
whitespace-normal
break-word
overscroll-contain
resize-none
`}
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder="Ask me anything..."
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
onSubmit();
event.preventDefault();
}
}}
suppressContentEditableWarning={true}
/>
<div className="absolute bottom-4 right-10">
<div className={"cursor-pointer"} onClick={() => onSubmit()}>
<FiSend
size={18}
className={
"text-emphasis w-9 h-9 p-2 rounded-lg " +
(message ? "bg-blue-200" : "")
}
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<DocumentSidebar
selectedMessage={aiMessage}
selectedDocuments={selectedDocuments}
setSelectedDocuments={setSelectedDocuments}
/>
</div>
);
};

View File

@ -0,0 +1,204 @@
import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS";
import { redirect } from "next/navigation";
import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, User, ValidSources } from "@/lib/types";
import { ChatSidebar } from "./sessionSidebar/ChatSidebar";
import { Chat } from "./Chat";
import {
BackendMessage,
ChatSession,
Message,
RetrievalType,
} from "./interfaces";
import { unstable_noStore as noStore } from "next/cache";
import { Persona } from "../admin/personas/interfaces";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { WelcomeModal } from "@/components/WelcomeModal";
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
export default async function ChatPage({
chatId,
shouldhideBeforeScroll,
}: {
chatId: string | null;
shouldhideBeforeScroll?: boolean;
}) {
noStore();
const currentChatId = chatId ? parseInt(chatId) : null;
const tasks = [
getAuthDisabledSS(),
getCurrentUserSS(),
fetchSS("/manage/connector"),
fetchSS("/manage/document-set"),
fetchSS("/persona?include_default=true"),
fetchSS("/chat/get-user-chat-sessions"),
chatId !== null
? fetchSS(`/chat/get-chat-session/${chatId}`)
: (async () => null)(),
];
// catch cases where the backend is completely unreachable here
// without try / catch, will just raise an exception and the page
// will not render
let results: (User | Response | boolean | null)[] = [
null,
null,
null,
null,
null,
null,
null,
];
try {
results = await Promise.all(tasks);
} catch (e) {
console.log(`Some fetch failed for the main search page - ${e}`);
}
const authDisabled = results[0] as boolean;
const user = results[1] as User | null;
const connectorsResponse = results[2] as Response | null;
const documentSetsResponse = results[3] as Response | null;
const personasResponse = results[4] as Response | null;
const chatSessionsResponse = results[5] as Response | null;
const chatSessionMessagesResponse = results[6] as Response | null;
if (!authDisabled && !user) {
return redirect("/auth/login");
}
let connectors: Connector<any>[] = [];
if (connectorsResponse?.ok) {
connectors = await connectorsResponse.json();
} else {
console.log(`Failed to fetch connectors - ${connectorsResponse?.status}`);
}
const availableSources: ValidSources[] = [];
connectors.forEach((connector) => {
if (!availableSources.includes(connector.source)) {
availableSources.push(connector.source);
}
});
let chatSessions: ChatSession[] = [];
if (chatSessionsResponse?.ok) {
chatSessions = (await chatSessionsResponse.json()).sessions;
} else {
console.log(
`Failed to fetch chat sessions - ${chatSessionsResponse?.text()}`
);
}
// Larger ID -> created later
chatSessions.sort((a, b) => (a.id > b.id ? -1 : 1));
const currentChatSession = chatSessions.find(
(chatSession) => chatSession.id === currentChatId
);
let documentSets: DocumentSet[] = [];
if (documentSetsResponse?.ok) {
documentSets = await documentSetsResponse.json();
} else {
console.log(
`Failed to fetch document sets - ${documentSetsResponse?.status}`
);
}
let personas: Persona[] = [];
if (personasResponse?.ok) {
personas = await personasResponse.json();
} else {
console.log(`Failed to fetch personas - ${personasResponse?.status}`);
}
let messages: Message[] = [];
if (chatSessionMessagesResponse?.ok) {
const chatSessionDetailJson = await chatSessionMessagesResponse.json();
const rawMessages = chatSessionDetailJson.messages as BackendMessage[];
const messageMap: Map<number, BackendMessage> = new Map(
rawMessages.map((message) => [message.message_id, message])
);
const rootMessage = rawMessages.find(
(message) => message.parent_message === null
);
const finalMessageList: BackendMessage[] = [];
if (rootMessage) {
let currMessage: BackendMessage | null = rootMessage;
while (currMessage) {
finalMessageList.push(currMessage);
const childMessageNumber = currMessage.latest_child_message;
if (childMessageNumber && messageMap.has(childMessageNumber)) {
currMessage = messageMap.get(childMessageNumber) as BackendMessage;
} else {
currMessage = null;
}
}
}
messages = finalMessageList
.filter((messageInfo) => messageInfo.message_type !== "system")
.map((messageInfo) => {
const hasContextDocs =
(messageInfo?.context_docs?.top_documents || []).length > 0;
let retrievalType;
if (hasContextDocs) {
if (messageInfo.rephrased_query) {
retrievalType = RetrievalType.Search;
} else {
retrievalType = RetrievalType.SelectedDocs;
}
} else {
retrievalType = RetrievalType.None;
}
return {
messageId: messageInfo.message_id,
message: messageInfo.message,
type: messageInfo.message_type as "user" | "assistant",
// only include these fields if this is an assistant message so that
// this is identical to what is computed at streaming time
...(messageInfo.message_type === "assistant"
? {
retrievalType: retrievalType,
query: messageInfo.rephrased_query,
documents: messageInfo?.context_docs?.top_documents || [],
citations: messageInfo?.citations || {},
}
: {}),
};
});
} else {
console.log(
`Failed to fetch chat session messages - ${chatSessionMessagesResponse?.text()}`
);
}
return (
<>
<InstantSSRAutoRefresh />
<ApiKeyModal />
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />}
<div className="flex relative bg-background text-default h-screen overflow-x-hidden">
<ChatSidebar
existingChats={chatSessions}
currentChatId={currentChatId}
user={user}
/>
<Chat
existingChatSessionId={currentChatId}
existingChatSessionPersonaId={currentChatSession?.persona_id}
existingMessages={messages}
availableSources={availableSources}
availableDocumentSets={documentSets}
availablePersonas={personas}
shouldhideBeforeScroll={shouldhideBeforeScroll}
/>
</div>
</>
);
}

View File

@ -0,0 +1,108 @@
import { Persona } from "@/app/admin/personas/interfaces";
import { FiCheck, FiChevronDown } from "react-icons/fi";
import { FaRobot } from "react-icons/fa";
import { CustomDropdown } from "@/components/Dropdown";
function PersonaItem({
id,
name,
onSelect,
isSelected,
}: {
id: number;
name: string;
onSelect: (personaId: number) => void;
isSelected: boolean;
}) {
return (
<div
key={id}
className={`
flex
px-3
text-sm
py-2
my-0.5
rounded
mx-1
select-none
cursor-pointer
text-emphasis
bg-background
hover:bg-hover
`}
onClick={() => {
onSelect(id);
}}
>
{name}
{isSelected && (
<div className="ml-auto mr-1">
<FiCheck />
</div>
)}
</div>
);
}
export function ChatPersonaSelector({
personas,
selectedPersonaId,
onPersonaChange,
}: {
personas: Persona[];
selectedPersonaId: number | null;
onPersonaChange: (persona: Persona | null) => void;
}) {
const currentlySelectedPersona = personas.find(
(persona) => persona.id === selectedPersonaId
);
return (
<CustomDropdown
dropdown={
<div
className={`
border
border-border
bg-background
rounded-lg
flex
flex-col
w-64
max-h-96
overflow-y-auto
flex
overscroll-contain`}
>
{personas.map((persona, ind) => {
const isSelected = persona.id === selectedPersonaId;
return (
<PersonaItem
key={persona.id}
id={persona.id}
name={persona.name}
onSelect={(clickedPersonaId) => {
const clickedPersona = personas.find(
(persona) => persona.id === clickedPersonaId
);
if (clickedPersona) {
onPersonaChange(clickedPersona);
}
}}
isSelected={isSelected}
/>
);
})}
</div>
}
>
<div className="select-none text-xl font-bold flex px-2 py-1.5 text-strong rounded cursor-pointer hover:bg-hover-light">
<div className="my-auto">
{currentlySelectedPersona?.name || "Default"}
</div>
<FiChevronDown className="my-auto ml-1" />
</div>
</CustomDropdown>
);
}

View File

@ -0,0 +1,14 @@
import ChatPage from "../ChatPage";
export default async function Page({
params,
searchParams,
}: {
params: { chatId: string };
searchParams: { shouldhideBeforeScroll?: string };
}) {
return await ChatPage({
chatId: params.chatId,
shouldhideBeforeScroll: searchParams.shouldhideBeforeScroll === "true",
});
}

View File

@ -0,0 +1,217 @@
import { HoverPopup } from "@/components/HoverPopup";
import { SourceIcon } from "@/components/SourceIcon";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { DocumentFeedbackBlock } from "@/components/search/DocumentFeedbackBlock";
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
import { DanswerDocument } from "@/lib/search/interfaces";
import { useState } from "react";
import { FiInfo, FiRadio } from "react-icons/fi";
import { DocumentSelector } from "./DocumentSelector";
export const buildDocumentSummaryDisplay = (
matchHighlights: string[],
blurb: string
) => {
if (matchHighlights.length === 0) {
return blurb;
}
// content, isBold, isContinuation
let sections = [] as [string, boolean, boolean][];
matchHighlights.forEach((matchHighlight, matchHighlightIndex) => {
if (!matchHighlight) {
return;
}
const words = matchHighlight.split(new RegExp("\\s"));
words.forEach((word) => {
if (!word) {
return;
}
let isContinuation = false;
while (word.includes("<hi>") && word.includes("</hi>")) {
const start = word.indexOf("<hi>");
const end = word.indexOf("</hi>");
const before = word.slice(0, start);
const highlight = word.slice(start + 4, end);
const after = word.slice(end + 5);
if (before) {
sections.push([before, false, isContinuation]);
isContinuation = true;
}
sections.push([highlight, true, isContinuation]);
isContinuation = true;
word = after;
}
if (word) {
sections.push([word, false, isContinuation]);
}
});
if (matchHighlightIndex != matchHighlights.length - 1) {
sections.push(["...", false, false]);
}
});
let previousIsContinuation = sections[0][2];
let previousIsBold = sections[0][1];
let currentText = "";
const finalJSX = [] as (JSX.Element | string)[];
sections.forEach(([word, shouldBeBold, isContinuation], index) => {
if (shouldBeBold != previousIsBold) {
if (currentText) {
if (previousIsBold) {
// remove leading space so that we don't bold the whitespace
// in front of the matching keywords
currentText = currentText.trim();
if (!previousIsContinuation) {
finalJSX[finalJSX.length - 1] = finalJSX[finalJSX.length - 1] + " ";
}
finalJSX.push(
<b key={index} className="text-default bg-highlight-text">
{currentText}
</b>
);
} else {
finalJSX.push(currentText);
}
}
currentText = "";
}
previousIsBold = shouldBeBold;
previousIsContinuation = isContinuation;
if (!isContinuation || index === 0) {
currentText += " ";
}
currentText += word;
});
if (currentText) {
if (previousIsBold) {
currentText = currentText.trim();
if (!previousIsContinuation) {
finalJSX[finalJSX.length - 1] = finalJSX[finalJSX.length - 1] + " ";
}
finalJSX.push(
<b key={sections.length} className="text-default bg-highlight-text">
{currentText}
</b>
);
} else {
finalJSX.push(currentText);
}
}
return finalJSX;
};
interface DocumentDisplayProps {
document: DanswerDocument;
queryEventId: number | null;
isAIPick: boolean;
isSelected: boolean;
handleSelect: (documentId: string) => void;
setPopup: (popupSpec: PopupSpec | null) => void;
}
export function ChatDocumentDisplay({
document,
queryEventId,
isAIPick,
isSelected,
handleSelect,
setPopup,
}: DocumentDisplayProps) {
const [isHovered, setIsHovered] = useState(false);
// Consider reintroducing null scored docs in the future
if (document.score === null) {
return null;
}
return (
<div
key={document.semantic_identifier}
className="text-sm px-3"
onMouseEnter={() => {
setIsHovered(true);
}}
onMouseLeave={() => setIsHovered(false)}
>
<div className="flex relative w-full overflow-x-hidden">
<a
className={
"rounded-lg flex font-bold flex-shrink truncate " +
(document.link ? "" : "pointer-events-none")
}
href={document.link}
target="_blank"
rel="noopener noreferrer"
>
<SourceIcon sourceType={document.source_type} iconSize={18} />
<p className="overflow-hidden text-ellipsis mx-2 my-auto text-sm ">
{document.semantic_identifier || document.document_id}
</p>
</a>
{document.score !== null && (
<div className="my-auto">
{isAIPick && (
<div className="w-4 h-4 my-auto mr-1 flex flex-col">
<HoverPopup
mainContent={<FiRadio className="text-gray-500 my-auto" />}
popupContent={
<div className="text-xs text-gray-300 w-36 flex">
<div className="flex mx-auto">
<div className="w-3 h-3 flex flex-col my-auto mr-1">
<FiInfo className="my-auto" />
</div>
<div className="my-auto">The AI liked this doc!</div>
</div>
</div>
}
direction="bottom"
style="dark"
/>
</div>
)}
<div
className={`
text-xs
text-emphasis
bg-hover
rounded
p-0.5
w-fit
my-auto
select-none
my-auto
mr-2`}
>
{Math.abs(document.score).toFixed(2)}
</div>
</div>
)}
<DocumentSelector
isSelected={isSelected}
handleSelect={() => handleSelect(document.document_id)}
/>
</div>
{document.updated_at && (
<DocumentUpdatedAtBadge updatedAt={document.updated_at} />
)}
<p className="pl-1 pt-2 pb-1 break-words">
{buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
</p>
<div className="mb-2">
{queryEventId && (
<DocumentFeedbackBlock
documentId={document.document_id}
queryId={queryEventId}
setPopup={setPopup}
/>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,23 @@
export function DocumentSelector({
isSelected,
handleSelect,
}: {
isSelected: boolean;
handleSelect: () => void;
}) {
return (
<div
className="ml-auto flex cursor-pointer select-none"
onClick={handleSelect}
>
<p className="mr-2 my-auto">Select</p>
<input
className="my-auto"
type="checkbox"
checked={isSelected}
// dummy function to prevent warning
onChange={() => null}
/>
</div>
);
}

View File

@ -0,0 +1,170 @@
import { DanswerDocument } from "@/lib/search/interfaces";
import { Text } from "@tremor/react";
import { ChatDocumentDisplay } from "./ChatDocumentDisplay";
import { usePopup } from "@/components/admin/connectors/Popup";
import { FiFileText, FiSearch } from "react-icons/fi";
import { SelectedDocumentDisplay } from "./SelectedDocumentDisplay";
import { removeDuplicateDocs } from "@/lib/documentUtils";
import { BasicSelectable } from "@/components/BasicClickable";
import { Message, RetrievalType } from "../interfaces";
function SectionHeader({
name,
icon,
}: {
name: string;
icon: React.FC<{ className: string }>;
}) {
return (
<div className="text-lg text-emphasis font-medium flex pb-0.5 mb-3.5 mt-2 font-bold">
{icon({ className: "my-auto mr-1" })}
{name}
</div>
);
}
export function DocumentSidebar({
selectedMessage,
selectedDocuments,
setSelectedDocuments,
}: {
selectedMessage: Message | null;
selectedDocuments: DanswerDocument[] | null;
setSelectedDocuments: (documents: DanswerDocument[]) => void;
}) {
const { popup, setPopup } = usePopup();
const selectedMessageRetrievalType = selectedMessage?.retrievalType || null;
const selectedDocumentIds =
selectedDocuments?.map((document) => document.document_id) || [];
const currentDocuments = selectedMessage?.documents || null;
const dedupedDocuments = removeDuplicateDocs(currentDocuments || []);
return (
<div
className={`
flex-initial
w-document-sidebar
border-l
border-border
overflow-y-hidden
flex
flex-col
pt-4
`}
id="document-sidebar"
>
{popup}
<div className="h-4/6 flex flex-col">
<div className="px-3 mb-3 flex border-b border-border">
<SectionHeader
name={
selectedMessageRetrievalType === RetrievalType.SelectedDocs
? "Referenced Documents"
: "Retrieved Documents"
}
icon={FiFileText}
/>
</div>
{currentDocuments ? (
<div className="overflow-y-auto overflow-x-hidden flex flex-col">
<div>
{dedupedDocuments.length > 0 ? (
dedupedDocuments.map((document, ind) => (
<div
key={document.document_id}
className={
ind === dedupedDocuments.length - 1
? "mb-5"
: "border-b border-border-light mb-3"
}
>
<ChatDocumentDisplay
document={document}
setPopup={setPopup}
queryEventId={null}
isAIPick={false}
isSelected={selectedDocumentIds.includes(
document.document_id
)}
handleSelect={(documentId) => {
if (selectedDocumentIds.includes(documentId)) {
setSelectedDocuments(
selectedDocuments!.filter(
(document) => document.document_id !== documentId
)
);
} else {
setSelectedDocuments([
...selectedDocuments!,
currentDocuments.find(
(document) => document.document_id === documentId
)!,
]);
}
}}
/>
</div>
))
) : (
<div className="mx-3">
<Text>No documents found for the query.</Text>
</div>
)}
</div>
</div>
) : (
<div className="ml-4">
<Text>
When you run ask a question, the retrieved documents will show up
here!
</Text>
</div>
)}
</div>
<div className="text-sm mb-4 border-t border-border pt-4 overflow-y-hidden flex flex-col">
<div className="flex border-b border-border px-3">
<div>
<SectionHeader name="Selected Documents" icon={FiFileText} />
</div>
{selectedDocuments && selectedDocuments.length > 0 && (
<div
className="ml-auto my-auto"
onClick={() => setSelectedDocuments([])}
>
<BasicSelectable selected={false}>De-Select All</BasicSelectable>
</div>
)}
</div>
{selectedDocuments && selectedDocuments.length > 0 ? (
<div className="flex flex-col gap-y-2 py-3 px-3 overflow-y-auto max-h-full">
{selectedDocuments.map((document) => (
<SelectedDocumentDisplay
key={document.document_id}
document={document}
handleDeselect={(documentId) => {
setSelectedDocuments(
selectedDocuments!.filter(
(document) => document.document_id !== documentId
)
);
}}
/>
))}
</div>
) : (
<Text className="mx-3 py-3">
Select documents from the retrieved documents section to chat
specifically with them!
</Text>
)}
</div>
</div>
);
}

View File

@ -0,0 +1,24 @@
import { SourceIcon } from "@/components/SourceIcon";
import { DanswerDocument } from "@/lib/search/interfaces";
import { DocumentSelector } from "./DocumentSelector";
export function SelectedDocumentDisplay({
document,
handleDeselect,
}: {
document: DanswerDocument;
handleDeselect: (documentId: string) => void;
}) {
return (
<div className="flex">
<SourceIcon sourceType={document.source_type} iconSize={18} />
<p className="truncate break-all mx-2 my-auto text-sm max-w-4/6">
{document.semantic_identifier || document.document_id}
</p>
<DocumentSelector
isSelected={true}
handleSelect={() => handleDeselect(document.document_id)}
/>
</div>
);
}

View File

@ -0,0 +1,54 @@
import { DanswerDocument, Filters } from "@/lib/search/interfaces";
export enum RetrievalType {
None = "none",
Search = "search",
SelectedDocs = "selectedDocs",
}
export interface RetrievalDetails {
run_search: "always" | "never" | "auto";
real_time: boolean;
filters?: Filters;
enable_auto_detect_filters?: boolean | null;
}
type CitationMap = { [key: string]: number };
export interface ChatSession {
id: number;
name: string;
persona_id: number;
time_created: string;
}
export interface Message {
messageId: number | null;
message: string;
type: "user" | "assistant" | "error";
retrievalType?: RetrievalType;
query?: string | null;
documents?: DanswerDocument[] | null;
citations?: CitationMap;
}
export interface BackendMessage {
message_id: number;
parent_message: number | null;
latest_child_message: number | null;
message: string;
rephrased_query: string | null;
context_docs: { top_documents: DanswerDocument[] } | null;
message_type: "user" | "assistant" | "system";
time_sent: string;
citations: CitationMap;
}
export interface DocumentsResponse {
top_documents: DanswerDocument[];
rephrased_query: string | null;
}
export interface StreamingError {
error: string;
}

268
web/src/app/chat/lib.tsx Normal file
View File

@ -0,0 +1,268 @@
import {
AnswerPiecePacket,
DanswerDocument,
Filters,
} from "@/lib/search/interfaces";
import { handleStream } from "@/lib/search/streamingUtils";
import { FeedbackType } from "./types";
import { RefObject } from "react";
import {
BackendMessage,
ChatSession,
DocumentsResponse,
Message,
StreamingError,
} from "./interfaces";
export async function createChatSession(personaId: number): Promise<number> {
const createChatSessionResponse = await fetch(
"/api/chat/create-chat-session",
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
persona_id: personaId,
}),
}
);
if (!createChatSessionResponse.ok) {
console.log(
`Failed to create chat session - ${createChatSessionResponse.status}`
);
throw Error("Failed to create chat session");
}
const chatSessionResponseJson = await createChatSessionResponse.json();
return chatSessionResponseJson.chat_session_id;
}
export interface SendMessageRequest {
message: string;
parentMessageId: number | null;
chatSessionId: number;
promptId: number | null | undefined;
filters: Filters | null;
selectedDocumentIds: number[] | null;
}
export async function* sendMessage({
message,
parentMessageId,
chatSessionId,
promptId,
filters,
selectedDocumentIds,
}: SendMessageRequest) {
const documentsAreSelected =
selectedDocumentIds && selectedDocumentIds.length > 0;
const sendMessageResponse = await fetch("/api/chat/send-message", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_session_id: chatSessionId,
parent_message_id: parentMessageId,
message: message,
prompt_id: promptId,
search_doc_ids: documentsAreSelected ? selectedDocumentIds : null,
retrieval_options: !documentsAreSelected
? {
run_search:
promptId === null || promptId === undefined ? "always" : "auto",
real_time: true,
filters: filters,
}
: null,
}),
});
if (!sendMessageResponse.ok) {
const errorJson = await sendMessageResponse.json();
const errorMsg = errorJson.message || errorJson.detail || "";
throw Error(`Failed to send message - ${errorMsg}`);
}
yield* handleStream<
AnswerPiecePacket | DocumentsResponse | BackendMessage | StreamingError
>(sendMessageResponse);
}
export async function nameChatSession(chatSessionId: number, message: string) {
const response = await fetch("/api/chat/rename-chat-session", {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_session_id: chatSessionId,
name: null,
first_message: message,
}),
});
return response;
}
export async function handleChatFeedback(
messageId: number,
feedback: FeedbackType,
feedbackDetails: string
) {
const response = await fetch("/api/chat/create-chat-message-feedback", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_message_id: messageId,
is_positive: feedback === "like",
feedback_text: feedbackDetails,
}),
});
return response;
}
export async function renameChatSession(
chatSessionId: number,
newName: string
) {
const response = await fetch(`/api/chat/rename-chat-session`, {
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
chat_session_id: chatSessionId,
name: newName,
first_message: null,
}),
});
return response;
}
export async function deleteChatSession(chatSessionId: number) {
const response = await fetch(
`/api/chat/delete-chat-session/${chatSessionId}`,
{
method: "DELETE",
}
);
return response;
}
export async function* simulateLLMResponse(input: string, delay: number = 30) {
// Split the input string into tokens. This is a simple example, and in real use case, tokenization can be more complex.
// Iterate over tokens and yield them one by one
const tokens = input.match(/.{1,3}|\n/g) || [];
for (const token of tokens) {
// In a real-world scenario, there might be a slight delay as tokens are being generated
await new Promise((resolve) => setTimeout(resolve, delay)); // 40ms delay to simulate response time
// Yielding each token
yield token;
}
}
export function handleAutoScroll(
endRef: RefObject<any>,
scrollableRef: RefObject<any>,
buffer: number = 300
) {
// Auto-scrolls if the user is within `buffer` of the bottom of the scrollableRef
if (endRef && endRef.current && scrollableRef && scrollableRef.current) {
if (
scrollableRef.current.scrollHeight -
scrollableRef.current.scrollTop -
buffer <=
scrollableRef.current.clientHeight
) {
endRef.current.scrollIntoView({ behavior: "smooth" });
}
}
}
export function getHumanAndAIMessageFromMessageNumber(
messageHistory: Message[],
messageId: number
) {
let messageInd;
// -1 is special -> means use the last message
if (messageId === -1) {
messageInd = messageHistory.length - 1;
} else {
messageInd = messageHistory.findIndex(
(message) => message.messageId === messageId
);
}
if (messageInd !== -1) {
const matchingMessage = messageHistory[messageInd];
const pairedMessage =
matchingMessage.type === "user"
? messageHistory[messageInd + 1]
: messageHistory[messageInd - 1];
const humanMessage =
matchingMessage.type === "user" ? matchingMessage : pairedMessage;
const aiMessage =
matchingMessage.type === "user" ? pairedMessage : matchingMessage;
return {
humanMessage,
aiMessage,
};
} else {
return {
humanMessage: null,
aiMessage: null,
};
}
}
export function getCitedDocumentsFromMessage(message: Message) {
if (!message.citations || !message.documents) {
return [];
}
const documentsWithCitationKey: [string, DanswerDocument][] = [];
Object.entries(message.citations).forEach(([citationKey, documentDbId]) => {
const matchingDocument = message.documents!.find(
(document) => document.db_doc_id === documentDbId
);
if (matchingDocument) {
documentsWithCitationKey.push([citationKey, matchingDocument]);
}
});
return documentsWithCitationKey;
}
export function groupSessionsByDateRange(chatSessions: ChatSession[]) {
const today = new Date();
today.setHours(0, 0, 0, 0); // Set to start of today for accurate comparison
const groups: Record<string, ChatSession[]> = {
Today: [],
"Previous 7 Days": [],
"Previous 30 Days": [],
"Over 30 days ago": [],
};
chatSessions.forEach((chatSession) => {
const chatSessionDate = new Date(chatSession.time_created);
const diffTime = today.getTime() - chatSessionDate.getTime();
const diffDays = diffTime / (1000 * 3600 * 24); // Convert time difference to days
if (diffDays < 1) {
groups["Today"].push(chatSession);
} else if (diffDays <= 7) {
groups["Previous 7 Days"].push(chatSession);
} else if (diffDays <= 30) {
groups["Previous 30 Days"].push(chatSession);
} else {
groups["Over 30 days ago"].push(chatSession);
}
});
return groups;
}

View File

@ -0,0 +1,235 @@
import {
FiCheck,
FiCopy,
FiCpu,
FiThumbsDown,
FiThumbsUp,
FiUser,
} from "react-icons/fi";
import { FeedbackType } from "../types";
import { useState } from "react";
import ReactMarkdown from "react-markdown";
import { DanswerDocument } from "@/lib/search/interfaces";
import { SearchSummary, ShowHideDocsButton } from "./SearchSummary";
import { SourceIcon } from "@/components/SourceIcon";
import { ThreeDots } from "react-loader-spinner";
const Hoverable: React.FC<{ children: JSX.Element; onClick?: () => void }> = ({
children,
onClick,
}) => {
return (
<div
className="hover:bg-neutral-300 p-2 rounded h-fit cursor-pointer"
onClick={onClick}
>
{children}
</div>
);
};
export const AIMessage = ({
messageId,
content,
query,
citedDocuments,
isComplete,
hasDocs,
handleFeedback,
isCurrentlyShowingRetrieved,
handleShowRetrieved,
}: {
messageId: number | null;
content: string | JSX.Element;
query?: string;
citedDocuments?: [string, DanswerDocument][] | null;
isComplete?: boolean;
hasDocs?: boolean;
handleFeedback?: (feedbackType: FeedbackType) => void;
isCurrentlyShowingRetrieved?: boolean;
handleShowRetrieved?: (messageNumber: number | null) => void;
}) => {
const [copyClicked, setCopyClicked] = useState(false);
return (
<div className={"py-5 px-5 flex -mr-6 w-full"}>
<div className="mx-auto w-searchbar relative">
<div className="ml-8">
<div className="flex">
<div className="p-1 bg-ai rounded-lg h-fit my-auto">
<div className="text-inverted">
<FiCpu size={16} className="my-auto mx-auto" />
</div>
</div>
<div className="font-bold text-emphasis ml-2 my-auto">Danswer</div>
{query === undefined &&
hasDocs &&
handleShowRetrieved !== undefined &&
isCurrentlyShowingRetrieved !== undefined && (
<div className="flex w-message-default absolute ml-8">
<div className="ml-auto">
<ShowHideDocsButton
messageId={messageId}
isCurrentlyShowingRetrieved={isCurrentlyShowingRetrieved}
handleShowRetrieved={handleShowRetrieved}
/>
</div>
</div>
)}
</div>
<div className="w-message-default break-words mt-1 ml-8">
{query !== undefined &&
handleShowRetrieved !== undefined &&
isCurrentlyShowingRetrieved !== undefined && (
<div className="my-1">
<SearchSummary
query={query}
hasDocs={hasDocs || false}
messageId={messageId}
isCurrentlyShowingRetrieved={isCurrentlyShowingRetrieved}
handleShowRetrieved={handleShowRetrieved}
/>
</div>
)}
{content ? (
<>
{typeof content === "string" ? (
<ReactMarkdown
className="prose max-w-full"
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-blue-500 hover:text-blue-700"
target="_blank"
rel="noopener noreferrer"
/>
),
}}
>
{content.replaceAll("\\n", "\n")}
</ReactMarkdown>
) : (
content
)}
</>
) : isComplete ? (
<div>I just performed the requested search!</div>
) : (
<div className="text-sm my-auto">
<ThreeDots
height="30"
width="50"
color="#3b82f6"
ariaLabel="grid-loading"
radius="12.5"
wrapperStyle={{}}
wrapperClass=""
visible={true}
/>
</div>
)}
{citedDocuments && citedDocuments.length > 0 && (
<div className="mt-2">
<b className="text-sm text-emphasis">Sources:</b>
<div className="flex flex-wrap gap-2">
{citedDocuments
.filter(([_, document]) => document.semantic_identifier)
.map(([citationKey, document], ind) => {
return (
<a
key={document.document_id}
href={document.link}
target="_blank"
className="text-sm border border-border py-1 px-2 rounded flex cursor-pointer hover:bg-hover"
>
<div className="max-w-350 text-ellipsis flex">
<div className="mr-1 my-auto">
<SourceIcon
sourceType={document.source_type}
iconSize={16}
/>
</div>
[{citationKey}] {document!.semantic_identifier}
</div>
</a>
);
})}
</div>
</div>
)}
</div>
{handleFeedback && (
<div className="flex flex-col md:flex-row gap-x-0.5 ml-8 mt-1">
<Hoverable
onClick={() => {
navigator.clipboard.writeText(content.toString());
setCopyClicked(true);
setTimeout(() => setCopyClicked(false), 3000);
}}
>
{copyClicked ? <FiCheck /> : <FiCopy />}
</Hoverable>
<Hoverable onClick={() => handleFeedback("like")}>
<FiThumbsUp />
</Hoverable>
<Hoverable>
<FiThumbsDown onClick={() => handleFeedback("dislike")} />
</Hoverable>
</div>
)}
</div>
</div>
</div>
);
};
export const HumanMessage = ({
content,
}: {
content: string | JSX.Element;
}) => {
return (
<div className="py-5 px-5 flex -mr-6 w-full">
<div className="mx-auto w-searchbar">
<div className="ml-8">
<div className="flex">
<div className="p-1 bg-user rounded-lg h-fit">
<div className="text-inverted">
<FiUser size={16} className="my-auto mx-auto" />
</div>
</div>
<div className="font-bold text-emphasis ml-2 my-auto">You</div>
</div>
<div className="mx-auto mt-1 ml-8 w-full w-message-default flex flex-wrap">
<div className="w-full sm:w-full w-message-default break-words">
{typeof content === "string" ? (
<ReactMarkdown
className="prose max-w-full"
components={{
a: ({ node, ...props }) => (
<a
{...props}
className="text-blue-500 hover:text-blue-700"
target="_blank"
rel="noopener noreferrer"
/>
),
}}
>
{content.replaceAll("\\n", "\n")}
</ReactMarkdown>
) : (
content
)}
</div>
</div>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,92 @@
import {
BasicClickable,
EmphasizedClickable,
} from "@/components/BasicClickable";
import { HoverPopup } from "@/components/HoverPopup";
import { DanswerDocument } from "@/lib/search/interfaces";
import { FiBookOpen, FiSearch } from "react-icons/fi";
export function ShowHideDocsButton({
messageId,
isCurrentlyShowingRetrieved,
handleShowRetrieved,
}: {
messageId: number | null;
isCurrentlyShowingRetrieved: boolean;
handleShowRetrieved: (messageId: number | null) => void;
}) {
return (
<div
className="ml-auto my-auto"
onClick={() => handleShowRetrieved(messageId)}
>
{isCurrentlyShowingRetrieved ? (
<EmphasizedClickable>
<div className="w-24 text-xs">Hide Docs</div>
</EmphasizedClickable>
) : (
<BasicClickable>
<div className="w-24 text-xs">Show Docs</div>
</BasicClickable>
)}
</div>
);
}
function SearchingForDisplay({
query,
isHoverable,
}: {
query: string;
isHoverable?: boolean;
}) {
return (
<div className={`flex p-1 rounded ${isHoverable && "cursor-default"}`}>
<FiSearch className="mr-2 my-auto" size={14} />
<p className="line-clamp-1 break-all w-96">
Searching for: <i>{query}</i>
</p>
</div>
);
}
export function SearchSummary({
query,
hasDocs,
messageId,
isCurrentlyShowingRetrieved,
handleShowRetrieved,
}: {
query: string;
hasDocs: boolean;
messageId: number | null;
isCurrentlyShowingRetrieved: boolean;
handleShowRetrieved: (messageId: number | null) => void;
}) {
return (
<div className="flex">
<div className="text-sm my-2">
{query.length >= 40 ? (
<HoverPopup
mainContent={<SearchingForDisplay query={query} isHoverable />}
popupContent={
<div className="w-96">
<b>Full query:</b> <div className="mt-1 italic">{query}</div>
</div>
}
direction="top"
/>
) : (
<SearchingForDisplay query={query} />
)}
</div>
{hasDocs && (
<ShowHideDocsButton
messageId={messageId}
isCurrentlyShowingRetrieved={isCurrentlyShowingRetrieved}
handleShowRetrieved={handleShowRetrieved}
/>
)}
</div>
);
}

View File

@ -0,0 +1,43 @@
import { FiTrash, FiX } from "react-icons/fi";
import { ModalWrapper } from "./ModalWrapper";
import { BasicClickable } from "@/components/BasicClickable";
export const DeleteChatModal = ({
chatSessionName,
onClose,
onSubmit,
}: {
chatSessionName: string;
onClose: () => void;
onSubmit: () => void;
}) => {
return (
<ModalWrapper onClose={onClose}>
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">Delete chat?</h2>
<div
onClick={onClose}
className="my-auto ml-auto p-2 hover:bg-hover rounded cursor-pointer"
>
<FiX size={20} />
</div>
</div>
<p className="mb-4">
Click below to confirm that you want to delete{" "}
<b>&quot;{chatSessionName.slice(0, 30)}&quot;</b>
</p>
<div className="flex">
<div className="mx-auto">
<BasicClickable onClick={onSubmit}>
<div className="flex mx-2">
<FiTrash className="my-auto mr-2" />
Delete
</div>
</BasicClickable>
</div>
</div>
</>
</ModalWrapper>
);
};

View File

@ -0,0 +1,84 @@
"use client";
import { useState } from "react";
import { FeedbackType } from "../types";
import { FiThumbsDown, FiThumbsUp } from "react-icons/fi";
import { ModalWrapper } from "./ModalWrapper";
interface FeedbackModalProps {
feedbackType: FeedbackType;
onClose: () => void;
onSubmit: (feedbackDetails: string) => void;
}
export const FeedbackModal = ({
feedbackType,
onClose,
onSubmit,
}: FeedbackModalProps) => {
const [message, setMessage] = useState("");
return (
<ModalWrapper onClose={onClose} modalClassName="max-w-5xl">
<>
<h2 className="text-2xl text-emphasis font-bold mb-4 flex">
<div className="mr-1 my-auto">
{feedbackType === "like" ? (
<FiThumbsUp className="text-green-500 my-auto mr-2" />
) : (
<FiThumbsDown className="text-red-600 my-auto mr-2" />
)}
</div>
Provide additional feedback
</h2>
<textarea
autoFocus
className={`
w-full
flex-grow
ml-2
border
border-border-strong
rounded
outline-none
placeholder-subtle
pl-4
pr-14
py-4
bg-background
overflow-hidden
h-28
whitespace-normal
resize-none
break-all
overscroll-contain`}
role="textarea"
aria-multiline
placeholder={
feedbackType === "like"
? "What did you like about this response?"
: "What was the issue with the response? How could it be improved?"
}
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter" && !event.shiftKey) {
onSubmit(message);
event.preventDefault();
}
}}
suppressContentEditableWarning={true}
/>
<div className="flex mt-2">
<button
className="bg-accent text-white py-2 px-4 rounded hover:bg-blue-600 focus:outline-none mx-auto"
onClick={() => onSubmit(message)}
>
Submit feedback
</button>
</div>
</>
</ModalWrapper>
);
};

View File

@ -0,0 +1,35 @@
export const ModalWrapper = ({
children,
bgClassName,
modalClassName,
onClose,
}: {
children: JSX.Element;
bgClassName?: string;
modalClassName?: string;
onClose?: () => void;
}) => {
return (
<div
onClick={() => onClose && onClose()}
className={
"fixed z-30 inset-0 overflow-y-auto bg-black bg-opacity-30 flex justify-center items-center " +
(bgClassName || "")
}
>
<div
onClick={(e) => {
if (onClose) {
e.stopPropagation();
}
}}
className={
"bg-background text-emphasis p-8 rounded shadow-xl w-3/4 max-w-3xl shadow " +
(modalClassName || "")
}
>
{children}
</div>
</div>
);
};

View File

@ -0,0 +1,345 @@
import React, { useEffect, useRef, useState } from "react";
import { DocumentSet, ValidSources } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import {
FiBook,
FiBookmark,
FiCalendar,
FiFilter,
FiMap,
FiX,
} from "react-icons/fi";
import { DateRangePickerValue } from "@tremor/react";
import { listSourceMetadata } from "@/lib/sources";
import { SourceIcon } from "@/components/SourceIcon";
import { BasicClickable } from "@/components/BasicClickable";
import { ControlledPopup, DefaultDropdownElement } from "@/components/Dropdown";
import { getXDaysAgo } from "@/lib/dateUtils";
enum FilterType {
Source = "Source",
KnowledgeSet = "Knowledge Set",
TimeRange = "Time Range",
}
interface SourceSelectorProps {
timeRange: DateRangePickerValue | null;
setTimeRange: React.Dispatch<
React.SetStateAction<DateRangePickerValue | null>
>;
selectedSources: SourceMetadata[];
setSelectedSources: React.Dispatch<React.SetStateAction<SourceMetadata[]>>;
selectedDocumentSets: string[];
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
availableDocumentSets: DocumentSet[];
existingSources: ValidSources[];
}
function SelectedBubble({
children,
onClick,
}: {
children: string | JSX.Element;
onClick: () => void;
}) {
return (
<div
className={
"flex text-xs cursor-pointer items-center border border-border " +
"py-1 rounded-lg px-2 w-fit select-none hover:bg-hover"
}
onClick={onClick}
>
{children}
<FiX className="ml-2" size={14} />
</div>
);
}
function SelectFilterType({
onSelect,
hasSources,
hasKnowledgeSets,
}: {
onSelect: (filterType: FilterType) => void;
hasSources: boolean;
hasKnowledgeSets: boolean;
}) {
return (
<div className="w-64">
{hasSources && (
<DefaultDropdownElement
key={FilterType.Source}
name={FilterType.Source}
icon={FiMap}
onSelect={() => onSelect(FilterType.Source)}
isSelected={false}
/>
)}
{hasKnowledgeSets && (
<DefaultDropdownElement
key={FilterType.KnowledgeSet}
name={FilterType.KnowledgeSet}
icon={FiBook}
onSelect={() => onSelect(FilterType.KnowledgeSet)}
isSelected={false}
/>
)}
<DefaultDropdownElement
key={FilterType.TimeRange}
name={FilterType.TimeRange}
icon={FiCalendar}
onSelect={() => onSelect(FilterType.TimeRange)}
isSelected={false}
/>
</div>
);
}
function SourcesSection({
sources,
selectedSources,
onSelect,
}: {
sources: SourceMetadata[];
selectedSources: string[];
onSelect: (source: SourceMetadata) => void;
}) {
return (
<div className="w-64">
{sources.map((source) => (
<DefaultDropdownElement
key={source.internalName}
name={source.displayName}
icon={source.icon}
onSelect={() => onSelect(source)}
isSelected={selectedSources.includes(source.internalName)}
includeCheckbox
/>
))}
</div>
);
}
function KnowledgeSetsSection({
documentSets,
selectedDocumentSets,
onSelect,
}: {
documentSets: DocumentSet[];
selectedDocumentSets: string[];
onSelect: (documentSetName: string) => void;
}) {
return (
<div className="w-64">
{documentSets.map((documentSet) => (
<DefaultDropdownElement
key={documentSet.name}
name={documentSet.name}
icon={FiBookmark}
onSelect={() => onSelect(documentSet.name)}
isSelected={selectedDocumentSets.includes(documentSet.name)}
includeCheckbox
/>
))}
</div>
);
}
const LAST_30_DAYS = "Last 30 days";
const LAST_7_DAYS = "Last 7 days";
const TODAY = "Today";
function TimeRangeSection({
selectedTimeRange,
onSelect,
}: {
selectedTimeRange: string | null;
onSelect: (timeRange: DateRangePickerValue) => void;
}) {
return (
<div className="w-64">
<DefaultDropdownElement
key={LAST_30_DAYS}
name={LAST_30_DAYS}
onSelect={() =>
onSelect({
to: new Date(),
from: getXDaysAgo(30),
selectValue: LAST_30_DAYS,
})
}
isSelected={selectedTimeRange === LAST_30_DAYS}
/>
<DefaultDropdownElement
key={LAST_7_DAYS}
name={LAST_7_DAYS}
onSelect={() =>
onSelect({
to: new Date(),
from: getXDaysAgo(7),
selectValue: LAST_7_DAYS,
})
}
isSelected={selectedTimeRange === LAST_7_DAYS}
/>
<DefaultDropdownElement
key={TODAY}
name={TODAY}
onSelect={() =>
onSelect({
to: new Date(),
from: getXDaysAgo(1),
selectValue: TODAY,
})
}
isSelected={selectedTimeRange === TODAY}
/>
</div>
);
}
export function ChatFilters({
timeRange,
setTimeRange,
selectedSources,
setSelectedSources,
selectedDocumentSets,
setSelectedDocumentSets,
availableDocumentSets,
existingSources,
}: SourceSelectorProps) {
const [filtersOpen, setFiltersOpen] = useState(false);
const handleFiltersToggle = (value: boolean) => {
setSelectedFilterType(null);
setFiltersOpen(value);
};
const [selectedFilterType, setSelectedFilterType] =
useState<FilterType | null>(null);
const handleSourceSelect = (source: SourceMetadata) => {
setSelectedSources((prev: SourceMetadata[]) => {
const prevSourceNames = prev.map((source) => source.internalName);
if (prevSourceNames.includes(source.internalName)) {
return prev.filter((s) => s.internalName !== source.internalName);
} else {
return [...prev, source];
}
});
};
const handleDocumentSetSelect = (documentSetName: string) => {
setSelectedDocumentSets((prev: string[]) => {
if (prev.includes(documentSetName)) {
return prev.filter((s) => s !== documentSetName);
} else {
return [...prev, documentSetName];
}
});
};
const allSources = listSourceMetadata();
const availableSources = allSources.filter((source) =>
existingSources.includes(source.internalName)
);
let popupDisplay = null;
if (selectedFilterType === FilterType.Source) {
popupDisplay = (
<SourcesSection
sources={availableSources}
selectedSources={selectedSources.map((source) => source.internalName)}
onSelect={handleSourceSelect}
/>
);
} else if (selectedFilterType === FilterType.KnowledgeSet) {
popupDisplay = (
<KnowledgeSetsSection
documentSets={availableDocumentSets}
selectedDocumentSets={selectedDocumentSets}
onSelect={handleDocumentSetSelect}
/>
);
} else if (selectedFilterType === FilterType.TimeRange) {
popupDisplay = (
<TimeRangeSection
selectedTimeRange={timeRange?.selectValue || null}
onSelect={(timeRange) => {
setTimeRange(timeRange);
handleFiltersToggle(!filtersOpen);
}}
/>
);
} else {
popupDisplay = (
<SelectFilterType
onSelect={(filterType) => setSelectedFilterType(filterType)}
hasSources={availableSources.length > 0}
hasKnowledgeSets={availableDocumentSets.length > 0}
/>
);
}
return (
<div className="flex">
<ControlledPopup
isOpen={filtersOpen}
setIsOpen={handleFiltersToggle}
popupContent={popupDisplay}
>
<div className="flex">
<BasicClickable onClick={() => handleFiltersToggle(!filtersOpen)}>
<div className="flex text-xs">
<FiFilter className="my-auto mr-1" /> Filter
</div>
</BasicClickable>
</div>
</ControlledPopup>
<div className="flex ml-4">
{((timeRange && timeRange.selectValue !== undefined) ||
selectedSources.length > 0 ||
selectedDocumentSets.length > 0) && (
<p className="text-xs my-auto mr-1">Currently applied:</p>
)}
<div className="flex flex-wrap gap-x-2">
{timeRange && timeRange.selectValue && (
<SelectedBubble onClick={() => setTimeRange(null)}>
<div className="flex">{timeRange.selectValue}</div>
</SelectedBubble>
)}
{existingSources.length > 0 &&
selectedSources.map((source) => (
<SelectedBubble
key={source.internalName}
onClick={() => handleSourceSelect(source)}
>
<>
<SourceIcon sourceType={source.internalName} iconSize={16} />
<span className="ml-2">{source.displayName}</span>
</>
</SelectedBubble>
))}
{selectedDocumentSets.length > 0 &&
selectedDocumentSets.map((documentSetName) => (
<SelectedBubble
key={documentSetName}
onClick={() => handleDocumentSetSelect(documentSetName)}
>
<>
<div>
<FiBookmark />
</div>
<span className="ml-2">{documentSetName}</span>
</>
</SelectedBubble>
))}
</div>
</div>
</div>
);
}

View File

@ -0,0 +1,71 @@
import { BasicClickable } from "@/components/BasicClickable";
import { ControlledPopup, DefaultDropdownElement } from "@/components/Dropdown";
import { useState } from "react";
import { FiCpu, FiFilter, FiSearch } from "react-icons/fi";
export const QA = "Question Answering";
export const SEARCH = "Search Only";
function SearchTypeSelectorContent({
selectedSearchType,
setSelectedSearchType,
}: {
selectedSearchType: string;
setSelectedSearchType: React.Dispatch<React.SetStateAction<string>>;
}) {
return (
<div className="w-56">
<DefaultDropdownElement
key={QA}
name={QA}
icon={FiCpu}
onSelect={() => setSelectedSearchType(QA)}
isSelected={selectedSearchType === QA}
/>
<DefaultDropdownElement
key={SEARCH}
name={SEARCH}
icon={FiSearch}
onSelect={() => setSelectedSearchType(SEARCH)}
isSelected={selectedSearchType === SEARCH}
/>
</div>
);
}
export function SearchTypeSelector({
selectedSearchType,
setSelectedSearchType,
}: {
selectedSearchType: string;
setSelectedSearchType: React.Dispatch<React.SetStateAction<string>>;
}) {
const [isOpen, setIsOpen] = useState(false);
return (
<ControlledPopup
isOpen={isOpen}
setIsOpen={setIsOpen}
popupContent={
<SearchTypeSelectorContent
selectedSearchType={selectedSearchType}
setSelectedSearchType={setSelectedSearchType}
/>
}
>
<BasicClickable onClick={() => setIsOpen(!isOpen)}>
<div className="flex text-xs">
{selectedSearchType === QA ? (
<>
<FiCpu className="my-auto mr-1" /> QA
</>
) : (
<>
<FiSearch className="my-auto mr-1" /> Search
</>
)}
</div>
</BasicClickable>
</ControlledPopup>
);
}

View File

@ -0,0 +1,25 @@
import { BasicClickable } from "@/components/BasicClickable";
import { DanswerDocument } from "@/lib/search/interfaces";
import { useState } from "react";
import { FiBook, FiFilter } from "react-icons/fi";
export function SelectedDocuments({
selectedDocuments,
}: {
selectedDocuments: DanswerDocument[];
}) {
if (selectedDocuments.length === 0) {
return null;
}
return (
<BasicClickable>
<div className="flex text-xs max-w-md overflow-hidden">
<FiBook className="my-auto mr-1" />{" "}
<div className="w-fit whitespace-nowrap">
Chatting with {selectedDocuments.length} Selected Documents
</div>
</div>
</BasicClickable>
);
}

12
web/src/app/chat/page.tsx Normal file
View File

@ -0,0 +1,12 @@
import ChatPage from "./ChatPage";
export default async function Page({
searchParams,
}: {
searchParams: { shouldhideBeforeScroll?: string };
}) {
return await ChatPage({
chatId: null,
shouldhideBeforeScroll: searchParams.shouldhideBeforeScroll === "true",
});
}

View File

@ -0,0 +1,196 @@
"use client";
import {
FiLogOut,
FiMessageSquare,
FiMoreHorizontal,
FiPlusSquare,
FiSearch,
FiTool,
} from "react-icons/fi";
import { useEffect, useRef, useState } from "react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { User } from "@/lib/types";
import { logout } from "@/lib/user";
import { BasicClickable, BasicSelectable } from "@/components/BasicClickable";
import Image from "next/image";
import { ChatSessionDisplay } from "./SessionDisplay";
import { ChatSession } from "../interfaces";
import { groupSessionsByDateRange } from "../lib";
interface ChatSidebarProps {
existingChats: ChatSession[];
currentChatId: number | null;
user: User | null;
}
export const ChatSidebar = ({
existingChats,
currentChatId,
user,
}: ChatSidebarProps) => {
const router = useRouter();
const groupedChatSessions = groupSessionsByDateRange(existingChats);
const [userInfoVisible, setUserInfoVisible] = useState(false);
const userInfoRef = useRef<HTMLDivElement>(null);
const handleLogout = () => {
logout().then((isSuccess) => {
if (!isSuccess) {
alert("Failed to logout");
}
router.push("/auth/login");
});
};
// hides logout popup on any click outside
const handleClickOutside = (event: MouseEvent) => {
if (
userInfoRef.current &&
!userInfoRef.current.contains(event.target as Node)
) {
setUserInfoVisible(false);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div
className={`
w-80
border-r
border-border
flex
flex-col
h-screen
transition-transform`}
id="chat-sidebar"
>
<Link href="/chat" className="ml-3">
<div className="flex mb-4 mt-4">
<div className="h-[32px] w-[30px]">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
<h1 className="flex text-2xl font-bold my-auto text-emphasis ml-2">
Danswer
</h1>
</div>
</Link>
<Link href="/chat" className="mx-3">
<BasicClickable fullWidth>
<div className="flex text-sm">
<FiPlusSquare className="my-auto mr-2" /> New Chat
</div>
</BasicClickable>
</Link>
<div className="mt-1 pb-1 mb-1 ml-3 overflow-y-auto h-full">
{Object.entries(groupedChatSessions).map(
([dateRange, chatSessions]) => {
if (chatSessions.length > 0) {
return (
<div key={dateRange}>
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-5 font-bold">
{dateRange}
</div>
{chatSessions.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={chat.id} className="mr-3">
<ChatSessionDisplay
chatSession={chat}
isSelected={isSelected}
/>
</div>
);
})}
</div>
);
}
}
)}
{/* {existingChats.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={chat.id} className="mr-3">
<ChatSessionDisplay chatSession={chat} isSelected={isSelected} />
</div>
);
})} */}
</div>
<div
className="mt-auto py-2 border-t border-border px-3"
ref={userInfoRef}
>
<div className="relative text-strong">
{userInfoVisible && (
<div
className={
(user ? "translate-y-[-110%]" : "translate-y-[-115%]") +
" absolute top-0 bg-background border border-border z-30 w-full rounded text-strong text-sm"
}
>
<Link
href="/search"
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
>
<FiSearch className="my-auto mr-2" />
Danswer Search
</Link>
<Link
href="/chat"
className="flex py-3 px-4 cursor-pointer hover:bg-hover"
>
<FiMessageSquare className="my-auto mr-2" />
Danswer Chat
</Link>
{(!user || user.role === "admin") && (
<Link
href="/admin/indexing/status"
className="flex py-3 px-4 cursor-pointer border-t border-border hover:bg-hover"
>
<FiTool className="my-auto mr-2" />
Admin Panel
</Link>
)}
{user && (
<div
onClick={handleLogout}
className="flex py-3 px-4 cursor-pointer border-t border-border rounded hover:bg-hover"
>
<FiLogOut className="my-auto mr-2" />
Log out
</div>
)}
</div>
)}
<BasicSelectable fullWidth selected={false}>
<div
onClick={() => setUserInfoVisible(!userInfoVisible)}
className="flex h-8"
>
<div className="my-auto mr-2 bg-user rounded-lg px-1.5">
{user && user.email ? user.email[0].toUpperCase() : "A"}
</div>
<p className="my-auto">
{user ? user.email : "Anonymous Possum"}
</p>
<FiMoreHorizontal className="my-auto ml-auto mr-2" size={20} />
</div>
</BasicSelectable>
</div>
</div>
</div>
);
};

View File

@ -0,0 +1,119 @@
import { useRouter } from "next/navigation";
import { ChatSession } from "../interfaces";
import { useState } from "react";
import { deleteChatSession, renameChatSession } from "../lib";
import { DeleteChatModal } from "../modal/DeleteChatModal";
import { BasicSelectable } from "@/components/BasicClickable";
import Link from "next/link";
import { FiCheck, FiEdit, FiMessageSquare, FiTrash, FiX } from "react-icons/fi";
interface ChatSessionDisplayProps {
chatSession: ChatSession;
isSelected: boolean;
}
export function ChatSessionDisplay({
chatSession,
isSelected,
}: ChatSessionDisplayProps) {
const router = useRouter();
const [isDeletionModalVisible, setIsDeletionModalVisible] = useState(false);
const [isRenamingChat, setIsRenamingChat] = useState(false);
const [chatName, setChatName] = useState(chatSession.name);
const onRename = async () => {
const response = await renameChatSession(chatSession.id, chatName);
if (response.ok) {
setIsRenamingChat(false);
router.refresh();
} else {
alert("Failed to rename chat session");
}
};
return (
<>
{isDeletionModalVisible && (
<DeleteChatModal
onClose={() => setIsDeletionModalVisible(false)}
onSubmit={async () => {
const response = await deleteChatSession(chatSession.id);
if (response.ok) {
setIsDeletionModalVisible(false);
// go back to the main page
router.push("/chat");
} else {
alert("Failed to delete chat session");
}
}}
chatSessionName={chatSession.name}
/>
)}
<Link
className="flex my-1"
key={chatSession.id}
href={`/chat/${chatSession.id}`}
>
<BasicSelectable fullWidth selected={isSelected}>
<div className="flex">
<div className="my-auto mr-2">
<FiMessageSquare size={16} />
</div>{" "}
{isRenamingChat ? (
<input
value={chatName}
onChange={(e) => setChatName(e.target.value)}
onKeyDown={(event) => {
if (event.key === "Enter") {
onRename();
event.preventDefault();
}
}}
className="-my-px px-1 mr-2 w-full rounded"
/>
) : (
<p className="text-ellipsis break-all line-clamp-1 mr-3 text-emphasis">
{chatName || `Chat ${chatSession.id}`}
</p>
)}
{isSelected &&
(isRenamingChat ? (
<div className="ml-auto my-auto flex">
<div
onClick={onRename}
className={`hover:bg-black/10 p-1 -m-1 rounded`}
>
<FiCheck size={16} />
</div>
<div
onClick={() => {
setChatName(chatSession.name);
setIsRenamingChat(false);
}}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2`}
>
<FiX size={16} />
</div>
</div>
) : (
<div className="ml-auto my-auto flex">
<div
onClick={() => setIsRenamingChat(true)}
className={`hover:bg-black/10 p-1 -m-1 rounded`}
>
<FiEdit size={16} />
</div>
<div
onClick={() => setIsDeletionModalVisible(true)}
className={`hover:bg-black/10 p-1 -m-1 rounded ml-2`}
>
<FiTrash size={16} />
</div>
</div>
))}
</div>
</BasicSelectable>
</Link>
</>
);
}

View File

@ -0,0 +1 @@
export type FeedbackType = "like" | "dislike";

View File

@ -1,3 +1,23 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
::-webkit-scrollbar-track {
background: #f9fafb; /* Track background color */
}
/* Style the scrollbar handle */
::-webkit-scrollbar-thumb {
background: #e5e7eb; /* Handle color */
border-radius: 10px;
}
/* Handle on hover */
::-webkit-scrollbar-thumb:hover {
background: #d1d5db; /* Handle color on hover */
}
::-webkit-scrollbar {
width: 8px; /* Vertical scrollbar width */
height: 8px; /* Horizontal scrollbar height */
}

View File

@ -21,7 +21,9 @@ export default async function RootLayout({
}) {
return (
<html lang="en">
<body className={`${inter.variable} font-sans bg-gray-900 text-gray-100`}>
<body
className={`${inter.variable} font-sans text-default bg-background`}
>
{children}
</body>
</html>

View File

@ -8,7 +8,7 @@ import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, User } from "@/lib/types";
import { cookies } from "next/headers";
import { SearchType } from "@/lib/search/interfaces";
import { Persona } from "./admin/personas/interfaces";
import { Persona } from "../admin/personas/interfaces";
import { WelcomeModal } from "@/components/WelcomeModal";
import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
@ -88,7 +88,7 @@ export default async function Home() {
<ApiKeyModal />
<InstantSSRAutoRefresh />
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />}
<div className="px-24 pt-10 flex flex-col items-center min-h-screen bg-gray-900 text-gray-100">
<div className="px-24 pt-10 flex flex-col items-center min-h-screen">
<div className="w-full">
<SearchSection
connectors={connectors}

View File

@ -13,7 +13,7 @@ export function BackButton() {
my-auto
flex
mb-1
hover:bg-gray-800
hover:bg-hover-light
w-fit
p-1
pr-2

View File

@ -0,0 +1,89 @@
export function BasicClickable({
children,
onClick,
fullWidth = false,
}: {
children: string | JSX.Element;
onClick?: () => void;
fullWidth?: boolean;
}) {
return (
<button
onClick={onClick}
className={`
border
border-border
shadow-md
rounded
font-medium
text-emphasis
text-sm
p-1
select-none
hover:bg-hover
${fullWidth ? "w-full" : ""}`}
>
{children}
</button>
);
}
export function EmphasizedClickable({
children,
onClick,
fullWidth = false,
}: {
children: string | JSX.Element;
onClick?: () => void;
fullWidth?: boolean;
}) {
return (
<button
onClick={onClick}
className={`
border
border-border
shadow-md
rounded
font-medium
text-emphasis
text-sm
p-1
select-none
bg-hover-light
hover:bg-hover
${fullWidth ? "w-full" : ""}`}
>
{children}
</button>
);
}
export function BasicSelectable({
children,
selected,
hasBorder,
fullWidth = false,
}: {
children: string | JSX.Element;
selected: boolean;
hasBorder?: boolean;
fullWidth?: boolean;
}) {
return (
<button
className={`
rounded
font-medium
text-emphasis
text-sm
p-1
select-none
${hasBorder ? "border border-border" : ""}
${selected ? "bg-hover" : "hover:bg-hover"}
${fullWidth ? "w-full" : ""}`}
>
{children}
</button>
);
}

View File

@ -0,0 +1,29 @@
export function Bubble({
isSelected,
onClick,
children,
}: {
isSelected: boolean;
onClick: () => void;
children: string | JSX.Element;
}) {
return (
<div
className={
`
px-3
py-1
rounded-lg
border
border-border
w-fit
flex
cursor-pointer ` +
(isSelected ? " bg-hover" : " bg-background hover:bg-hover-light")
}
onClick={onClick}
>
<div className="my-auto">{children}</div>
</div>
);
}

View File

@ -16,13 +16,13 @@ export const CustomCheckbox = ({
/>
<span className="relative">
<span
className={`block w-3 h-3 border border-gray-600 rounded ${
checked ? "bg-green-700" : "bg-gray-800"
className={`block w-3 h-3 border border-border-strong rounded ${
checked ? "bg-green-700" : "bg-background"
} transition duration-300`}
>
{checked && (
<svg
className="absolute top-0 left-0 w-3 h-3 fill-current text-gray-200"
className="absolute top-0 left-0 w-3 h-3 fill-current text-inverted"
viewBox="0 0 20 20"
>
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />

View File

@ -0,0 +1,28 @@
import { FiTrash } from "react-icons/fi";
export function DeleteButton({
onClick,
disabled,
}: {
onClick?: () => void;
disabled?: boolean;
}) {
return (
<div
className={`
my-auto
flex
mb-1
${disabled ? "cursor-default" : "hover:bg-hover cursor-pointer"}
w-fit
p-2
rounded-lg
border-border
text-sm`}
onClick={onClick}
>
<FiTrash className="mr-1 my-auto" />
Delete
</div>
);
}

View File

@ -314,7 +314,7 @@ export const CustomDropdown = ({
{isOpen && (
<div
onClick={() => setIsOpen(!isOpen)}
className="pt-2 absolute bottom w-full z-30 bg-gray-900"
className="pt-2 absolute bottom w-full z-30 box-shadow"
>
{dropdown}
</div>
@ -323,48 +323,52 @@ export const CustomDropdown = ({
);
};
function DefaultDropdownElement({
id,
export function DefaultDropdownElement({
name,
icon,
description,
onSelect,
isSelected,
isFinal,
includeCheckbox = false,
}: {
id: string | number | null;
name: string;
icon?: React.FC<{ size?: number; className?: string }>;
description?: string;
onSelect: (value: string | number | null) => void;
isSelected: boolean;
isFinal: boolean;
onSelect?: () => void;
isSelected?: boolean;
includeCheckbox?: boolean;
}) {
console.log(isFinal);
return (
<div
className={`
flex
px-3
mx-1
px-2
text-sm
text-gray-200
py-2.5
py-1.5
my-1
select-none
cursor-pointer
${isFinal ? "" : "border-b border-gray-800"}
${
isSelected
? "bg-dark-tremor-background-muted"
: "hover:bg-dark-tremor-background-muted "
}
bg-background
rounded
hover:bg-hover-light
`}
onClick={() => {
onSelect(id);
}}
onClick={onSelect}
>
<div>
{name}
{description && (
<div className="text-xs text-dark-tremor-content">{description}</div>
<div className="flex">
{includeCheckbox && (
<input
type="checkbox"
className="mr-2"
checked={isSelected}
onChange={() => null}
/>
)}
{icon && icon({ size: 16, className: "mr-2 my-auto" })}
{name}
</div>
{description && <div className="text-xs">{description}</div>}
</div>
{isSelected && (
<div className="ml-auto mr-1 my-auto">
@ -394,10 +398,11 @@ export function DefaultDropdown({
<div
className={`
border
border-gray-800
border
rounded-lg
flex
flex-col
bg-background
max-h-96
overflow-y-auto
overscroll-contain`}
@ -405,13 +410,11 @@ export function DefaultDropdown({
{includeDefault && (
<DefaultDropdownElement
key={-1}
id={null}
name="Default"
onSelect={() => {
onSelect(null);
}}
isSelected={selected === null}
isFinal={false}
/>
)}
{options.map((option, ind) => {
@ -419,12 +422,10 @@ export function DefaultDropdown({
return (
<DefaultDropdownElement
key={option.value}
id={option.value}
name={option.name}
description={option.description}
onSelect={onSelect}
onSelect={() => onSelect(option.value)}
isSelected={isSelected}
isFinal={ind === options.length - 1}
/>
);
})}
@ -435,16 +436,15 @@ export function DefaultDropdown({
className={`
flex
text-sm
text-gray-400
bg-background
px-3
py-1.5
rounded-lg
border
border-gray-800
cursor-pointer
hover:bg-dark-tremor-background-muted`}
border-border
cursor-pointer`}
>
<p className="text-gray-200 line-clamp-1">
<p className="line-clamp-1">
{selectedOption?.name ||
(includeDefault ? "Default" : "Select an option...")}
</p>
@ -453,3 +453,45 @@ export function DefaultDropdown({
</CustomDropdown>
);
}
export function ControlledPopup({
children,
popupContent,
isOpen,
setIsOpen,
}: {
children: JSX.Element | string;
popupContent: JSX.Element | string;
isOpen: boolean;
setIsOpen: (value: boolean) => void;
}) {
const filtersRef = useRef<HTMLDivElement>(null);
// hides logout popup on any click outside
const handleClickOutside = (event: MouseEvent) => {
if (
filtersRef.current &&
!filtersRef.current.contains(event.target as Node)
) {
setIsOpen(false);
}
};
useEffect(() => {
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, []);
return (
<div ref={filtersRef} className="relative">
{children}
{isOpen && (
<div className="absolute top-0 translate-y-[-105%] bg-background border border-border z-30 rounded text-emphasis">
{popupContent}
</div>
)}
</div>
);
}

View File

@ -1,8 +1,6 @@
"use client";
import { useRouter } from "next/navigation";
import { FiChevronLeft, FiEdit } from "react-icons/fi";
import { FiEdit } from "react-icons/fi";
export function EditButton({ onClick }: { onClick: () => void }) {
return (
@ -11,12 +9,12 @@ export function EditButton({ onClick }: { onClick: () => void }) {
my-auto
flex
mb-1
hover:bg-gray-800
hover:bg-hover
w-fit
p-2
cursor-pointer
rounded-lg
border-gray-800
border-border
text-sm`}
onClick={onClick}
>

View File

@ -7,6 +7,8 @@ import Image from "next/image";
import Link from "next/link";
import { useRouter } from "next/navigation";
import React, { useEffect, useRef, useState } from "react";
import { CustomDropdown, DefaultDropdownElement } from "./Dropdown";
import { FiMessageSquare, FiSearch } from "react-icons/fi";
interface HeaderProps {
user: User | null;
@ -51,50 +53,78 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
}, [dropdownOpen]);
return (
<header className="bg-gray-800 text-gray-200 py-4">
<div className="mx-8 flex">
<Link href="/">
<header className="border-b border-border bg-background-emphasis">
<div className="mx-8 flex h-16">
<Link className="py-4" href="/search">
<div className="flex">
<div className="h-[32px] w-[30px]">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div>
<h1 className="flex text-2xl font-bold my-auto">Danswer</h1>
<h1 className="flex text-2xl text-strong font-bold my-auto">
Danswer
</h1>
</div>
</Link>
<div
className="ml-auto flex items-center cursor-pointer relative"
onClick={() => setDropdownOpen(!dropdownOpen)}
ref={dropdownRef}
<Link
href="/search"
className="ml-8 h-full flex flex-col hover:bg-hover"
>
<UserCircle size={24} className="mr-1 hover:text-red-500" />
{dropdownOpen && (
<div className="w-28 flex my-auto">
<div className="mx-auto flex text-strong px-2">
<FiSearch className="my-auto mr-1" />
<h1 className="flex text-base font-medium my-auto">Search</h1>
</div>
</div>
</Link>
<Link href="/chat" className="h-full flex flex-col hover:bg-hover">
<div className="w-28 flex my-auto">
<div className="mx-auto flex text-strong px-2">
<FiMessageSquare className="my-auto mr-1" />
<h1 className="flex text-base font-medium my-auto">Chat</h1>
</div>
</div>
</Link>
<div className="ml-auto h-full flex flex-col">
<div className="my-auto">
<CustomDropdown
dropdown={
<div
className={
"absolute top-10 right-0 mt-2 bg-gray-600 rounded-sm " +
"w-48 overflow-hidden shadow-xl z-10 text-sm text-gray-300"
"absolute right-0 mt-2 bg-background rounded border border-border " +
"w-48 overflow-hidden shadow-xl z-10 text-sm"
}
>
{/* Show connector option if (1) auth is disabled or (2) user is an admin */}
{(!user || user.role === "admin") && (
<Link href="/admin/indexing/status">
<div className="flex py-2 px-3 cursor-pointer hover:bg-gray-500 border-b border-gray-500">
Admin Panel
</div>
<DefaultDropdownElement name="Admin Panel" />
</Link>
)}
{user && (
<div
className="flex py-2 px-3 cursor-pointer hover:bg-gray-500"
onClick={handleLogout}
<DefaultDropdownElement
name="Logout"
onSelect={handleLogout}
/>
)}
</div>
}
>
Logout
<div className="hover:bg-hover rounded p-1 w-fit">
<div className="my-auto bg-user text-sm rounded-lg px-1.5 select-none">
{user && user.email ? user.email[0].toUpperCase() : "A"}
</div>
)}
</div>
)}
</CustomDropdown>
</div>
</div>
</div>
</header>
);
};
/*
*/

View File

@ -4,7 +4,7 @@ interface HoverPopupProps {
mainContent: string | JSX.Element;
popupContent: string | JSX.Element;
classNameModifications?: string;
direction?: "left" | "bottom";
direction?: "left" | "bottom" | "top";
style?: "basic" | "dark";
}
@ -25,6 +25,9 @@ export const HoverPopup = ({
case "bottom":
popupDirectionClass = "top-0 left-0 mt-6 pt-2";
break;
case "top":
popupDirectionClass = "top-0 left-0 translate-y-[-100%] pb-2";
break;
}
return (
@ -39,10 +42,7 @@ export const HoverPopup = ({
<div className={`absolute ${popupDirectionClass} z-30`}>
<div
className={
`px-3 py-2 rounded ` +
(style === "dark"
? "bg-dark-tremor-background-muted border border-gray-800"
: "bg-gray-800 shadow-lg") +
`px-3 py-2 rounded bg-background border border-border` +
(classNameModifications || "")
}
>
@ -50,7 +50,7 @@ export const HoverPopup = ({
</div>
</div>
)}
<div className="z-20">{mainContent}</div>
<div>{mainContent}</div>
</div>
);
};

View File

@ -22,14 +22,14 @@ export function Modal({
>
<div
className={`
bg-gray-800 rounded-sm shadow-lg
bg-background rounded-sm shadow-lg
shadow-lg relative w-1/2 text-sm
${className}
`}
onClick={(event) => event.stopPropagation()}
>
{title && (
<h2 className="text-xl font-bold mb-3 border-b border-gray-700 pt-4 pb-3 bg-gray-700 px-6">
<h2 className="text-xl font-bold mb-3 border-b border-border pt-4 pb-3 bg-background-strong px-6">
{title}
</h2>
)}

View File

@ -61,14 +61,13 @@ const PageLink = ({
py-1
leading-5
-ml-px
text-gray-300
border-gray-600
${!unclickable ? "hover:bg-gray-600" : ""}
border-border
${!unclickable ? "hover:bg-hover" : ""}
${!unclickable ? "cursor-pointer" : ""}
first:ml-0
first:rounded-l-md
last:rounded-r-md
${active ? "bg-gray-700" : ""}
${active ? "bg-background-strong" : ""}
`}
onClick={() => {
if (pageChangeHandler) {

View File

@ -61,8 +61,7 @@ export function IndexAttemptStatus({
);
}
// TODO: remove wrapping `dark` once we have light/dark mode
return <div className="dark">{badge}</div>;
return <div>{badge}</div>;
}
export function CCPairStatus({
@ -104,6 +103,5 @@ export function CCPairStatus({
);
}
// TODO: remove wrapping `dark` once we have light/dark mode
return <div className="dark">{badge}</div>;
return <div>{badge}</div>;
}

View File

@ -7,11 +7,11 @@ import Link from "next/link";
export function WelcomeModal() {
return (
<Modal className="max-w-4xl">
<div className="px-6 py-4">
<h2 className="text-xl font-bold mb-4 pb-2 border-b border-gray-700 flex">
<div className="px-8 py-6">
<h2 className="text-xl font-bold mb-4 pb-2 border-b border-border flex">
Welcome to Danswer 🎉
</h2>
<div className="text-gray-100">
<div>
<p className="mb-4">
Danswer is the AI-powered search engine for your organization&apos;s
internal knowledge. Whenever you need to find any piece of internal
@ -26,7 +26,7 @@ export function WelcomeModal() {
</p>
</div>
<div className="flex mt-3 dark">
<div className="flex mt-3">
<Link href="/admin/add-connector" className="mx-auto">
<Button>Setup your first connector!</Button>
</Link>

View File

@ -1,5 +1,5 @@
import { Header } from "@/components/Header";
import { Sidebar } from "@/components/admin/connectors/Sidebar";
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
import {
NotebookIcon,
KeyIcon,
@ -30,11 +30,13 @@ export async function Layout({ children }: { children: React.ReactNode }) {
}
return (
<div>
<div className="h-screen overflow-y-hidden">
<div className="absolute top-0 z-50 w-full">
<Header user={user} />
<div className="bg-gray-900 pt-8 pb-8 flex">
<div className="w-72">
<Sidebar
</div>
<div className="flex h-full pt-16">
<div className="w-80 pt-12 pb-8 h-full border-r border-border">
<AdminSidebar
collections={[
{
name: "Connectors",
@ -145,7 +147,9 @@ export async function Layout({ children }: { children: React.ReactNode }) {
]}
/>
</div>
<div className="px-12 bg-gray-900 text-gray-100 w-full">{children}</div>
<div className="px-12 pt-8 pb-8 h-full overflow-y-auto w-full">
{children}
</div>
</div>
</div>
);

View File

@ -13,12 +13,12 @@ export function AdminPageTitle({
includeDivider?: boolean;
}) {
return (
<div className="dark">
<div>
<div className="mb-4">
<HealthCheckBanner />
</div>
<div className="flex">
<h1 className="text-3xl font-bold flex gap-x-2">
<h1 className="text-3xl text-strong font-bold flex gap-x-2">
{icon} {title}
</h1>
{farRightElement && <div className="ml-auto">{farRightElement}</div>}

Some files were not shown because too many files have changed in this diff Show More