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 shared: bool
system_prompt: str system_prompt: str
task_prompt: str task_prompt: str
include_citations: bool include_citations: bool = False
datetime_aware: bool datetime_aware: bool = False
persona_ids: list[int] persona_ids: list[int] | None = None
class PromptSnapshot(BaseModel): class PromptSnapshot(BaseModel):

View File

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

View File

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

View File

@ -24,20 +24,35 @@ const nextConfig = {
// In production, something else (nginx in the one box setup) should take // In production, something else (nginx in the one box setup) should take
// care of this redirect. TODO (chris): better support setups where // care of this redirect. TODO (chris): better support setups where
// web_server and api_server are on different machines. // web_server and api_server are on different machines.
if (process.env.NODE_ENV === "production") return []; const defaultRedirects = [
return [
{ {
source: "/api/stream-direct-qa:params*", source: "/",
destination: "http://127.0.0.1:8080/stream-direct-qa:params*", // Proxy to Backend destination: "/search",
permanent: true,
},
{
source: "/api/stream-query-validation:params*",
destination: "http://127.0.0.1:8080/stream-query-validation:params*", // Proxy to Backend
permanent: true, 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: { publicRuntimeConfig: {
version, 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 flex-col
items-center items-center
justify-center justify-center
bg-dark-tremor-background-muted
p-4 p-4
rounded-lg rounded-lg
w-40 w-40
cursor-pointer cursor-pointer
bg-hover-light
shadow-md shadow-md
hover:bg-gray-800 hover:bg-hover
`} `}
href={sourceMetadata.adminUrl} href={sourceMetadata.adminUrl}
> >
<SourceIcon sourceType={sourceMetadata.internalName} iconSize={24} /> <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} {sourceMetadata.displayName}
</span> </Text>
</Link> </Link>
); );
} }
@ -42,26 +42,26 @@ export default function Page() {
); );
return ( return (
<div className="mx-auto container dark"> <div className="mx-auto container">
<AdminPageTitle <AdminPageTitle
icon={<ConnectorIcon size={32} />} icon={<ConnectorIcon size={32} />}
title="Add Connector" title="Add Connector"
/> />
<div className="text-gray-300 text-sm"> <Text>
Connect Danswer to your organization&apos;s knowledge sources. Connect Danswer to your organization&apos;s knowledge sources.
We&apos;ll automatically sync your data into Danswer, so you can find We&apos;ll automatically sync your data into Danswer, so you can find
exactly what you&apos;re looking for in one place. exactly what you&apos;re looking for in one place.
</div> </Text>
<div className="flex mt-8"> <div className="flex mt-8">
<Title>Import Knowledge</Title> <Title>Import Knowledge</Title>
</div> </div>
<div className="text-gray-300 text-sm"> <Text>
Connect to pieces of knowledge that live outside your apps. Upload Connect to pieces of knowledge that live outside your apps. Upload
files, scrape websites, or connect to your organization&apos;s Google files, scrape websites, or connect to your organization&apos;s Google
Site. Site.
</div> </Text>
<div className="flex flex-wrap gap-4 p-4"> <div className="flex flex-wrap gap-4 p-4">
{importedKnowledgeSources.map((source) => { {importedKnowledgeSources.map((source) => {
return ( return (
@ -73,11 +73,11 @@ export default function Page() {
<div className="flex mt-8"> <div className="flex mt-8">
<Title>Setup Auto-Syncing from Apps</Title> <Title>Setup Auto-Syncing from Apps</Title>
</div> </div>
<div className="text-gray-300 text-sm"> <Text>
Setup auto-syncing from your organization&apos;s most used apps and Setup auto-syncing from your organization&apos;s most used apps and
services. Unless otherwise specified during the connector setup, we will services. Unless otherwise specified during the connector setup, we will
pull in the latest updates from the source every 10 minutes. pull in the latest updates from the source every 10 minutes.
</div> </Text>
<div className="flex flex-wrap gap-4 p-4"> <div className="flex flex-wrap gap-4 p-4">
{appConnectionSources.map((source) => { {appConnectionSources.map((source) => {
return ( return (

View File

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

View File

@ -11,7 +11,7 @@ import {
setSlackBotTokens, setSlackBotTokens,
updateSlackBotConfig, updateSlackBotConfig,
} from "./lib"; } from "./lib";
import { Card } from "@tremor/react"; import { Button, Card } from "@tremor/react";
interface SlackBotTokensFormProps { interface SlackBotTokensFormProps {
onClose: () => void; onClose: () => void;
@ -64,17 +64,9 @@ export const SlackBotTokensForm = ({
type="password" type="password"
/> />
<div className="flex"> <div className="flex">
<button <Button type="submit" disabled={isSubmitting}>
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"
}
>
Set Tokens Set Tokens
</button> </Button>
</div> </div>
</Form> </Form>
)} )}

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
import { getNameFromPath } from "@/lib/fileUtils"; import { getNameFromPath } from "@/lib/fileUtils";
import { ValidSources } from "@/lib/types"; 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 { function convertObjectToString(obj: any): string | any {
// Check if obj is an object and not an array or null // 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> <div>
{popup} {popup}
<Button <Button
variant="secondary"
size="xs" size="xs"
color="red" color="red"
onClick={() => onClick={() =>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,8 @@ import { Form, Formik } from "formik";
import { TextFormField } from "@/components/admin/connectors/Field"; import { TextFormField } from "@/components/admin/connectors/Field";
import { FileUpload } from "@/components/admin/connectors/FileUpload"; import { FileUpload } from "@/components/admin/connectors/FileUpload";
import { getNameFromPath } from "@/lib/fileUtils"; import { getNameFromPath } from "@/lib/fileUtils";
import { Button, Card, Divider, Text } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => { const Main = () => {
const [selectedFiles, setSelectedFiles] = useState<File[]>([]); const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
@ -48,13 +50,13 @@ const Main = () => {
<div> <div>
{popup} {popup}
{filesAreUploading && <Spinner />} {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 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>,{" "} 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) <i>.pdf</i> and <i>.zip</i> files (containing only <i>.txt</i> files)
are supported. are supported.
</p> </Text>
<div className="text-sm mb-3"> <Text className="mb-3">
<b>NOTE:</b> if the original document is accessible via a link, you can <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: add a line at the very beginning of the file that looks like:
<div className="flex my-2"> <div className="flex my-2">
@ -67,163 +69,172 @@ const Main = () => {
search result. More details on this can be found in the{" "} search result. More details on this can be found in the{" "}
<a <a
href="https://docs.danswer.dev/connectors/file" href="https://docs.danswer.dev/connectors/file"
className="text-blue-500" className="text-link"
> >
documentation. documentation.
</a> </a>
</div> </Text>
<div className="flex mt-4"> <div className="flex mt-4">
<div className="mx-auto max-w-3xl w-full"> <div className="mx-auto w-full">
<Formik <Card>
initialValues={{ <Formik
name: "", initialValues={{
selectedFiles: [], name: "",
}} selectedFiles: [],
validationSchema={Yup.object().shape({ }}
name: Yup.string().required( validationSchema={Yup.object().shape({
"Please enter a descriptive name for the files" name: Yup.string().required(
), "Please enter a descriptive name for the files"
})} ),
onSubmit={async (values, formikHelpers) => { })}
const uploadCreateAndTriggerConnector = async () => { onSubmit={async (values, formikHelpers) => {
const formData = new FormData(); const uploadCreateAndTriggerConnector = async () => {
const formData = new FormData();
selectedFiles.forEach((file) => { selectedFiles.forEach((file) => {
formData.append("files", file); formData.append("files", file);
});
const response = await fetch(
"/api/manage/admin/connector/file/upload",
{ method: "POST", body: formData }
);
const responseJson = await response.json();
if (!response.ok) {
setPopup({
message: `Unable to upload files - ${responseJson.detail}`,
type: "error",
}); });
return;
}
const filePaths = responseJson.file_paths as string[]; const response = await fetch(
const [connectorErrorMsg, connector] = "/api/manage/admin/connector/file/upload",
await createConnector<FileConfig>({ { method: "POST", body: formData }
name: "FileConnector-" + Date.now(), );
source: "file", const responseJson = await response.json();
input_type: "load_state", if (!response.ok) {
connector_specific_config: { setPopup({
file_locations: filePaths, message: `Unable to upload files - ${responseJson.detail}`,
}, type: "error",
refresh_freq: null, });
disabled: false, return;
});
if (connectorErrorMsg || !connector) {
setPopup({
message: `Unable to create connector - ${connectorErrorMsg}`,
type: "error",
});
return;
}
// Since there is no "real" credential associated with a file connector
// we create a dummy one here so that we can associate the CC Pair with a
// user. This is needed since the user for a CC Pair is found via the credential
// associated with it.
const createCredentialResponse = await createCredential({
credential_json: {},
admin_public: true,
});
if (!createCredentialResponse.ok) {
const errorMsg = await createCredentialResponse.text();
setPopup({
message: `Error creating credential for CC Pair - ${errorMsg}`,
type: "error",
});
formikHelpers.setSubmitting(false);
return;
}
const credentialId = (await createCredentialResponse.json()).id;
const credentialResponse = await linkCredential(
connector.id,
credentialId,
values.name
);
if (!credentialResponse.ok) {
const credentialResponseJson =
await credentialResponse.json();
setPopup({
message: `Unable to link connector to credential - ${credentialResponseJson.detail}`,
type: "error",
});
return;
}
const runConnectorErrorMsg = await runConnector(connector.id, [
0,
]);
if (runConnectorErrorMsg) {
setPopup({
message: `Unable to run connector - ${runConnectorErrorMsg}`,
type: "error",
});
return;
}
mutate("/api/manage/admin/connector/indexing-status");
setSelectedFiles([]);
formikHelpers.resetForm();
setPopup({
type: "success",
message: "Successfully uploaded files!",
});
};
setFilesAreUploading(true);
try {
await uploadCreateAndTriggerConnector();
} catch (e) {
console.log("Failed to index filels: ", e);
}
setFilesAreUploading(false);
}}
>
{({ values, isSubmitting }) => (
<Form className="p-3 border border-gray-600 rounded">
<h2 className="font-bold text-xl mb-2">Upload Files</h2>
<TextFormField
name="name"
label="Name:"
placeholder={`A name that describes the files e.g. "Onboarding Documents"`}
autoCompleteDisabled={true}
/>
<p className="mb-1">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"
} }
type="submit"
disabled={ const filePaths = responseJson.file_paths as string[];
selectedFiles.length === 0 || !values.name || isSubmitting const [connectorErrorMsg, connector] =
await createConnector<FileConfig>({
name: "FileConnector-" + Date.now(),
source: "file",
input_type: "load_state",
connector_specific_config: {
file_locations: filePaths,
},
refresh_freq: null,
disabled: false,
});
if (connectorErrorMsg || !connector) {
setPopup({
message: `Unable to create connector - ${connectorErrorMsg}`,
type: "error",
});
return;
} }
>
Upload! // Since there is no "real" credential associated with a file connector
</button> // we create a dummy one here so that we can associate the CC Pair with a
</Form> // user. This is needed since the user for a CC Pair is found via the credential
)} // associated with it.
</Formik> const createCredentialResponse = await createCredential({
credential_json: {},
admin_public: true,
});
if (!createCredentialResponse.ok) {
const errorMsg = await createCredentialResponse.text();
setPopup({
message: `Error creating credential for CC Pair - ${errorMsg}`,
type: "error",
});
formikHelpers.setSubmitting(false);
return;
}
const credentialId = (await createCredentialResponse.json())
.id;
const credentialResponse = await linkCredential(
connector.id,
credentialId,
values.name
);
if (!credentialResponse.ok) {
const credentialResponseJson =
await credentialResponse.json();
setPopup({
message: `Unable to link connector to credential - ${credentialResponseJson.detail}`,
type: "error",
});
return;
}
const runConnectorErrorMsg = await runConnector(
connector.id,
[0]
);
if (runConnectorErrorMsg) {
setPopup({
message: `Unable to run connector - ${runConnectorErrorMsg}`,
type: "error",
});
return;
}
mutate("/api/manage/admin/connector/indexing-status");
setSelectedFiles([]);
formikHelpers.resetForm();
setPopup({
type: "success",
message: "Successfully uploaded files!",
});
};
setFilesAreUploading(true);
try {
await uploadCreateAndTriggerConnector();
} catch (e) {
console.log("Failed to index filels: ", e);
}
setFilesAreUploading(false);
}}
>
{({ values, isSubmitting }) => (
<Form>
<h2 className="font-bold text-emphasis text-xl mb-2">
Upload Files
</h2>
<TextFormField
name="name"
label="Name:"
placeholder={`A name that describes the files e.g. "Onboarding Documents"`}
autoCompleteDisabled={true}
/>
<p className="mb-1 font-medium text-emphasis">Files:</p>
<FileUpload
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
/>
<div className="flex">
<Button
className="mt-4 w-64 mx-auto"
color="green"
size="xs"
type="submit"
disabled={
selectedFiles.length === 0 ||
!values.name ||
isSubmitting
}
>
Upload!
</Button>
</div>
</Form>
)}
</Formik>
</Card>
</div> </div>
</div> </div>
{fileIndexingStatuses.length > 0 && ( {fileIndexingStatuses.length > 0 && (
<div className="mt-6"> <div>
<Divider />
<h2 className="font-bold text-xl mb-2">Indexed Files</h2> <h2 className="font-bold text-xl mb-2">Indexed Files</h2>
<SingleUseConnectorsTable<FileConfig, {}> <SingleUseConnectorsTable<FileConfig, {}>
connectorIndexingStatuses={fileIndexingStatuses} connectorIndexingStatuses={fileIndexingStatuses}
@ -253,10 +264,9 @@ export default function File() {
<div className="mb-4"> <div className="mb-4">
<HealthCheckBanner /> <HealthCheckBanner />
</div> </div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<FileIcon size={32} /> <AdminPageTitle icon={<FileIcon size={32} />} title="File" />
<h1 className="text-3xl font-bold pl-2">File</h1>
</div>
<Main /> <Main />
</div> </div>
); );

View File

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

View File

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

View File

@ -7,6 +7,8 @@ import { XIcon } from "@/components/icons/icons";
import { Connector, GoogleDriveConfig } from "@/lib/types"; import { Connector, GoogleDriveConfig } from "@/lib/types";
import * as Yup from "yup"; import * as Yup from "yup";
import { googleDriveConnectorNameBuilder } from "./utils"; import { googleDriveConnectorNameBuilder } from "./utils";
import { Modal } from "@/components/Modal";
import { Divider, Text } from "@tremor/react";
interface Props { interface Props {
existingConnector: Connector<GoogleDriveConfig>; existingConnector: Connector<GoogleDriveConfig>;
@ -15,25 +17,28 @@ interface Props {
export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => { export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
return ( return (
<div <Modal onOutsideClick={onSubmit}>
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50" <div className="px-8 py-6 bg-background">
onClick={onSubmit} <h2 className="text-xl font-bold flex">
> Update Google Drive Connector
<div <div
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm" onClick={onSubmit}
onClick={(event) => event.stopPropagation()} className="ml-auto hover:bg-hover p-1.5 rounded"
> >
<div className="flex border-b border-gray-600 pb-2 mb-2">
<h3 className="text-lg font-semibold w-full">
Update Google Drive Connector
</h3>
<div onClick={onSubmit}>
<XIcon <XIcon
size={30} size={20}
className="my-auto flex flex-shrink-0 cursor-pointer hover:text-blue-400" className="my-auto flex flex-shrink-0 cursor-pointer"
/> />
</div> </div>
</div> </h2>
<Text>
Modify the selected Google Drive connector by adjusting the values
below!
</Text>
<Divider />
<UpdateConnectorForm<GoogleDriveConfig> <UpdateConnectorForm<GoogleDriveConfig>
nameBuilder={googleDriveConnectorNameBuilder} nameBuilder={googleDriveConnectorNameBuilder}
existingConnector={existingConnector} existingConnector={existingConnector}
@ -67,6 +72,6 @@ export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
onSubmit={onSubmit} onSubmit={onSubmit}
/> />
</div> </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 Cookies from "js-cookie";
import { TextFormField } from "@/components/admin/connectors/Field"; import { TextFormField } from "@/components/admin/connectors/Field";
import { Form, Formik } from "formik"; import { Form, Formik } from "formik";
import { Card } from "@tremor/react";
type GoogleDriveCredentialJsonTypes = "authorized_user" | "service_account"; type GoogleDriveCredentialJsonTypes = "authorized_user" | "service_account";
@ -246,7 +247,7 @@ export const DriveJsonUploadSection = ({
<p className="text-sm mb-2"> <p className="text-sm mb-2">
Follow the guide{" "} Follow the guide{" "}
<a <a
className="text-blue-500" className="text-link"
target="_blank" target="_blank"
href="https://docs.danswer.dev/connectors/google_drive#authorization" 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. the documents you want to index with the service account.
</p> </p>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4"> <Card>
<Formik <Formik
initialValues={{ initialValues={{
google_drive_delegated_user: "", google_drive_delegated_user: "",
@ -385,7 +386,7 @@ export const DriveOAuthSection = ({
</Form> </Form>
)} )}
</Formik> </Formik>
</div> </Card>
</div> </div>
); );
} }

View File

@ -13,6 +13,14 @@ import { useSWRConfig } from "swr";
import { useState } from "react"; import { useState } from "react";
import { ConnectorEditPopup } from "./ConnectorEditPopup"; import { ConnectorEditPopup } from "./ConnectorEditPopup";
import { DeleteColumn } from "@/components/admin/connectors/table/DeleteColumn"; import { DeleteColumn } from "@/components/admin/connectors/table/DeleteColumn";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
interface EditableColumnProps { interface EditableColumnProps {
connectorIndexingStatus: ConnectorIndexingStatus< connectorIndexingStatus: ConnectorIndexingStatus<
@ -44,7 +52,7 @@ const EditableColumn = ({ connectorIndexingStatus }: EditableColumnProps) => {
className="cursor-pointer" className="cursor-pointer"
> >
<div className="mr-2"> <div className="mr-2">
<EditIcon size={20} /> <EditIcon size={16} />
</div> </div>
</div> </div>
</div> </div>
@ -74,6 +82,99 @@ export const GoogleDriveConnectorsTable = ({
(a, b) => a.connector.id - b.connector.id (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 ( return (
<BasicTable <BasicTable
columns={[ columns={[

View File

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

View File

@ -17,6 +17,8 @@ import { linkCredential } from "@/lib/credential";
import { FileUpload } from "@/components/admin/connectors/FileUpload"; import { FileUpload } from "@/components/admin/connectors/FileUpload";
import { SingleUseConnectorsTable } from "@/components/admin/connectors/table/SingleUseConnectorsTable"; import { SingleUseConnectorsTable } from "@/components/admin/connectors/table/SingleUseConnectorsTable";
import { Spinner } from "@/components/Spinner"; import { Spinner } from "@/components/Spinner";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button, Card, Text, Title } from "@tremor/react";
export default function GoogleSites() { export default function GoogleSites() {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
@ -50,11 +52,13 @@ export default function GoogleSites() {
<div className="mb-4"> <div className="mb-4">
<HealthCheckBanner /> <HealthCheckBanner />
</div> </div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<GoogleSitesIcon size={32} /> <AdminPageTitle
<h1 className="text-3xl font-bold pl-2">Google Sites</h1> icon={<GoogleSitesIcon size={32} />}
</div> title="Google Sites"
<p className="text-sm mb-2"> />
<Text className="mb-2">
For an in-depth guide on how to setup this connector, check out{" "} For an in-depth guide on how to setup this connector, check out{" "}
<a <a
href="https://docs.danswer.dev/connectors/google_sites" href="https://docs.danswer.dev/connectors/google_sites"
@ -64,140 +68,142 @@ export default function GoogleSites() {
the documentation the documentation
</a> </a>
. .
</p> </Text>
<div className="mt-4"> <div className="mt-4">
<h2 className="font-bold text-xl mb-2">Upload Files</h2> <Title className="mb-2">Upload Files</Title>
<div className="mx-auto w-full"> <Card>
<Formik <div className="mx-auto w-full">
initialValues={{ <Formik
base_url: "", initialValues={{
}} base_url: "",
validationSchema={Yup.object().shape({ }}
base_url: Yup.string().required("Base URL is required"), validationSchema={Yup.object().shape({
})} base_url: Yup.string().required("Base URL is required"),
onSubmit={async (values, formikHelpers) => { })}
const uploadCreateAndTriggerConnector = async () => { onSubmit={async (values, formikHelpers) => {
const formData = new FormData(); const uploadCreateAndTriggerConnector = async () => {
const formData = new FormData();
selectedFiles.forEach((file) => { selectedFiles.forEach((file) => {
formData.append("files", file); formData.append("files", file);
});
const response = await fetch(
"/api/manage/admin/connector/file/upload",
{ method: "POST", body: formData }
);
const responseJson = await response.json();
if (!response.ok) {
setPopup({
message: `Unable to upload files - ${responseJson.detail}`,
type: "error",
}); });
return;
}
const filePaths = responseJson.file_paths as string[]; const response = await fetch(
const [connectorErrorMsg, connector] = "/api/manage/admin/connector/file/upload",
await createConnector<GoogleSitesConfig>({ { method: "POST", body: formData }
name: `GoogleSitesConnector-${values.base_url}`, );
source: "google_sites", const responseJson = await response.json();
input_type: "load_state", if (!response.ok) {
connector_specific_config: { setPopup({
base_url: values.base_url, message: `Unable to upload files - ${responseJson.detail}`,
zip_path: filePaths[0], type: "error",
}, });
refresh_freq: null, return;
disabled: false,
});
if (connectorErrorMsg || !connector) {
setPopup({
message: `Unable to create connector - ${connectorErrorMsg}`,
type: "error",
});
return;
}
const credentialResponse = await linkCredential(
connector.id,
0,
values.base_url
);
if (!credentialResponse.ok) {
const credentialResponseJson =
await credentialResponse.json();
setPopup({
message: `Unable to link connector to credential - ${credentialResponseJson.detail}`,
type: "error",
});
return;
}
const runConnectorErrorMsg = await runConnector(
connector.id,
[0]
);
if (runConnectorErrorMsg) {
setPopup({
message: `Unable to run connector - ${runConnectorErrorMsg}`,
type: "error",
});
return;
}
mutate("/api/manage/admin/connector/indexing-status");
setSelectedFiles([]);
formikHelpers.resetForm();
setPopup({
type: "success",
message: "Successfully uploaded files!",
});
};
setFilesAreUploading(true);
try {
await uploadCreateAndTriggerConnector();
} catch (e) {
console.log("Failed to index filels: ", e);
}
setFilesAreUploading(false);
}}
>
{({ values, isSubmitting }) => (
<Form className="p-3 border border-gray-600 rounded">
<TextFormField
name="base_url"
label="Base URL:"
placeholder={`Base URL of your Google Site e.g. https://sites.google.com/view/your-site`}
subtext="This will be used to generate links for each page."
autoCompleteDisabled={true}
/>
<p className="mb-1 font-medium">Files:</p>
<FileUpload
selectedFiles={selectedFiles}
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"
} }
type="submit"
disabled={ const filePaths = responseJson.file_paths as string[];
selectedFiles.length !== 1 || const [connectorErrorMsg, connector] =
!values.base_url || await createConnector<GoogleSitesConfig>({
isSubmitting name: `GoogleSitesConnector-${values.base_url}`,
source: "google_sites",
input_type: "load_state",
connector_specific_config: {
base_url: values.base_url,
zip_path: filePaths[0],
},
refresh_freq: null,
disabled: false,
});
if (connectorErrorMsg || !connector) {
setPopup({
message: `Unable to create connector - ${connectorErrorMsg}`,
type: "error",
});
return;
} }
>
Upload! const credentialResponse = await linkCredential(
</button> connector.id,
</Form> 0,
)} values.base_url
</Formik> );
</div> if (!credentialResponse.ok) {
const credentialResponseJson =
await credentialResponse.json();
setPopup({
message: `Unable to link connector to credential - ${credentialResponseJson.detail}`,
type: "error",
});
return;
}
const runConnectorErrorMsg = await runConnector(
connector.id,
[0]
);
if (runConnectorErrorMsg) {
setPopup({
message: `Unable to run connector - ${runConnectorErrorMsg}`,
type: "error",
});
return;
}
mutate("/api/manage/admin/connector/indexing-status");
setSelectedFiles([]);
formikHelpers.resetForm();
setPopup({
type: "success",
message: "Successfully uploaded files!",
});
};
setFilesAreUploading(true);
try {
await uploadCreateAndTriggerConnector();
} catch (e) {
console.log("Failed to index filels: ", e);
}
setFilesAreUploading(false);
}}
>
{({ values, isSubmitting }) => (
<Form>
<TextFormField
name="base_url"
label="Base URL:"
placeholder={`Base URL of your Google Site e.g. https://sites.google.com/view/your-site`}
subtext="This will be used to generate links for each page."
autoCompleteDisabled={true}
/>
<p className="mb-1 font-medium">Files:</p>
<FileUpload
selectedFiles={selectedFiles}
setSelectedFiles={setSelectedFiles}
message="Upload a zip file containing the HTML of your Google Site"
/>
<div className="flex">
<Button
className="mt-4 w-64 mx-auto"
size="xs"
color="green"
type="submit"
disabled={
selectedFiles.length !== 1 ||
!values.base_url ||
isSubmitting
}
>
Upload!
</Button>
</div>
</Form>
)}
</Formik>
</div>
</Card>
</div> </div>
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto"> <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 { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { usePublicCredentials } from "@/lib/hooks"; import { usePublicCredentials } from "@/lib/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
import { Card, Text, Title } from "@tremor/react";
const Main = () => { const Main = () => {
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
@ -70,23 +72,23 @@ const Main = () => {
return ( return (
<> <>
{popup} {popup}
<p className="text-sm"> <Text>
This connector allows you to sync all your Guru Cards into Danswer. 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 Step 1: Provide your Credentials
</h2> </Title>
{guruCredential ? ( {guruCredential ? (
<> <>
<div className="flex mb-1 text-sm"> <div className="flex mb-1 text-sm">
<p className="my-auto">Existing Access Token: </p> <Text className="my-auto">Existing Access Token: </Text>
<p className="ml-1 italic my-auto max-w-md truncate"> <Text className="ml-1 italic my-auto max-w-md truncate">
{guruCredential.credential_json?.guru_user_token} {guruCredential.credential_json?.guru_user_token}
</p> </Text>
<button <button
className="ml-1 hover:bg-gray-700 rounded-full p-1" className="ml-1 hover:bg-hover rounded p-1"
onClick={async () => { onClick={async () => {
if (guruConnectorIndexingStatuses.length > 0) { if (guruConnectorIndexingStatuses.length > 0) {
setPopup({ setPopup({
@ -106,18 +108,18 @@ const Main = () => {
</> </>
) : ( ) : (
<> <>
<p className="text-sm"> <Text>
To use the Guru connector, first follow the guide{" "} To use the Guru connector, first follow the guide{" "}
<a <a
className="text-blue-500" className="text-link"
href="https://help.getguru.com/s/article/how-to-obtain-your-api-credentials" href="https://help.getguru.com/s/article/how-to-obtain-your-api-credentials"
target="_blank" target="_blank"
> >
here here
</a>{" "} </a>{" "}
to generate a User Token. to generate a User Token.
</p> </Text>
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2"> <Card className="mt-4">
<CredentialForm<GuruCredentialJson> <CredentialForm<GuruCredentialJson>
formBody={ 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! Step 2: Start indexing!
</h2> </Title>
{guruCredential ? ( {guruCredential ? (
!guruConnectorIndexingStatuses.length ? ( !guruConnectorIndexingStatuses.length ? (
<> <>
<p className="text-sm mb-2"> <Text className="mb-2">
Click the button below to start indexing! We will pull the latest Click the button below to start indexing! We will pull the latest
features, components, and products from Guru every <b>10</b>{" "} features, components, and products from Guru every <b>10</b>{" "}
minutes. minutes.
</p> </Text>
<div className="flex"> <div className="flex">
<ConnectorForm<GuruConfig> <ConnectorForm<GuruConfig>
nameBuilder={() => "GuruConnector"} 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 Guru connector is setup! We are pulling the latest cards from Guru
every <b>10</b> minutes. every <b>10</b> minutes.
</p> </Text>
<ConnectorsTable<GuruConfig, GuruCredentialJson> <ConnectorsTable<GuruConfig, GuruCredentialJson>
connectorIndexingStatuses={guruConnectorIndexingStatuses} connectorIndexingStatuses={guruConnectorIndexingStatuses}
liveCredential={guruCredential} 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 Please provide your access token in Step 1 first! Once done with
that, you can then start indexing all your Guru cards. that, you can then start indexing all your Guru cards.
</p> </Text>
</> </>
)} )}
</> </>
@ -222,10 +224,9 @@ export default function Page() {
<div className="mb-4"> <div className="mb-4">
<HealthCheckBanner /> <HealthCheckBanner />
</div> </div>
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
<GuruIcon size={32} /> <AdminPageTitle icon={<GuruIcon size={32} />} title="Guru" />
<h1 className="text-3xl font-bold pl-2">Guru</h1>
</div>
<Main /> <Main />
</div> </div>
); );

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ const DocumentDisplay = ({
return ( return (
<div <div
key={document.document_id} 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"> <div className="flex relative">
<a <a
@ -49,7 +49,7 @@ const DocumentDisplay = ({
</a> </a>
</div> </div>
<div className="flex flex-wrap gap-x-2 mt-1 text-xs"> <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> <p className="mr-1 my-auto">Boost:</p>
<ScoreSection <ScoreSection
documentId={document.document_id} 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"> <div className="my-auto">
{document.hidden ? ( {document.hidden ? (
<div className="text-red-500">Hidden</div> <div className="text-error">Hidden</div>
) : ( ) : (
"Visible" "Visible"
)} )}
@ -95,7 +95,7 @@ const DocumentDisplay = ({
<DocumentUpdatedAtBadge updatedAt={document.updated_at} /> <DocumentUpdatedAtBadge updatedAt={document.updated_at} />
</div> </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)} {buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
</p> </p>
</div> </div>
@ -159,11 +159,11 @@ export function Explorer({
<div> <div>
{popup} {popup}
<div className="justify-center py-2"> <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"> <div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent">
<MagnifyingGlass className="text-gray-400" /> <MagnifyingGlass />
<textarea <textarea
autoFocus 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" role="textarea"
aria-multiline aria-multiline
placeholder="Find documents based on title / content..." placeholder="Find documents based on title / content..."
@ -180,7 +180,7 @@ export function Explorer({
suppressContentEditableWarning={true} suppressContentEditableWarning={true}
/> />
</div> </div>
<div className="mt-4"> <div className="mt-4 border-b border-border">
<HorizontalFilters <HorizontalFilters
{...filterManager} {...filterManager}
availableDocumentSets={documentSets} availableDocumentSets={documentSets}
@ -203,7 +203,7 @@ export function Explorer({
</div> </div>
)} )}
{!query && ( {!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 Search for a document above to modify it&apos;s boost or hide it from
searches. searches.
</div> </div>

View File

@ -1,6 +1,14 @@
import { BasicTable } from "@/components/admin/connectors/BasicTable"; import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { useState } from "react"; import { useState } from "react";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
} from "@tremor/react";
import { PageSelector } from "@/components/PageSelector"; import { PageSelector } from "@/components/PageSelector";
import { DocumentBoostStatus } from "@/lib/types"; import { DocumentBoostStatus } from "@/lib/types";
import { updateHiddenStatus } from "../lib"; import { updateHiddenStatus } from "../lib";
@ -30,7 +38,7 @@ const IsVisibleSection = ({
); );
onUpdate(response); 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="select-none">Hidden</div>
<div className="ml-1 my-auto"> <div className="ml-1 my-auto">
@ -46,9 +54,9 @@ const IsVisibleSection = ({
); );
onUpdate(response); 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"> <div className="ml-1 my-auto">
<CustomCheckbox checked={true} /> <CustomCheckbox checked={true} />
</div> </div>
@ -56,7 +64,7 @@ const IsVisibleSection = ({
) )
} }
popupContent={ popupContent={
<div className="text-xs text-gray-300"> <div className="text-xs">
{document.hidden ? ( {document.hidden ? (
<div className="flex"> <div className="flex">
<FiEye className="my-auto mr-1" /> Unhide <FiEye className="my-auto mr-1" /> Unhide
@ -86,69 +94,68 @@ export const DocumentFeedbackTable = ({
return ( return (
<div> <div>
{popup} <Table className="overflow-visible">
<BasicTable <TableHead>
columns={[ <TableRow>
{ <TableHeaderCell>Document Name</TableHeaderCell>
header: "Document Name", <TableHeaderCell>Is Searchable?</TableHeaderCell>
key: "name", <TableHeaderCell>Score</TableHeaderCell>
}, </TableRow>
{ </TableHead>
header: "Is Searchable?", <TableBody>
key: "visible", {documents
}, .slice((page - 1) * numToDisplay, page * numToDisplay)
{ .map((document) => {
header: "Score", return (
key: "score", <TableRow key={document.document_id}>
alignment: "right", <TableCell className="whitespace-normal break-all">
}, <a
]} className="text-blue-600"
data={documents href={document.link}
.slice((page - 1) * numToDisplay, page * numToDisplay) target="_blank"
.map((document) => { rel="noopener noreferrer"
return { >
name: ( {document.semantic_id}
<a </a>
className="text-blue-600" </TableCell>
href={document.link} <TableCell>
target="_blank" <IsVisibleSection
rel="noopener noreferrer" document={document}
> onUpdate={async (response) => {
{document.semantic_id} if (response.ok) {
</a> refresh();
), } else {
visible: ( setPopup({
<IsVisibleSection message: `Error updating hidden status - ${getErrorMsg(
document={document} response
onUpdate={async (response) => { )}`,
if (response.ok) { type: "error",
refresh(); });
} else { }
setPopup({ }}
message: `Error updating hidden status - ${getErrorMsg(
response
)}`,
type: "error",
});
}
}}
/>
),
score: (
<div className="ml-auto flex w-16">
<div key={document.document_id} className="h-10 ml-auto mr-8">
<ScoreSection
documentId={document.document_id}
initialScore={document.boost}
refresh={refresh}
setPopup={setPopup}
/> />
</div> </TableCell>
</div> <TableCell>
), <div className="ml-auto flex w-16">
}; <div
})} key={document.document_id}
/> className="h-10 ml-auto mr-8"
>
<ScoreSection
documentId={document.document_id}
initialScore={document.boost}
refresh={refresh}
setPopup={setPopup}
/>
</div>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="mt-3 flex"> <div className="mt-3 flex">
<div className="mx-auto"> <div className="mx-auto">
<PageSelector <PageSelector

View File

@ -6,6 +6,7 @@ import { useMostReactedToDocuments } from "@/lib/hooks";
import { DocumentFeedbackTable } from "./DocumentFeedbackTable"; import { DocumentFeedbackTable } from "./DocumentFeedbackTable";
import { numPages, numToDisplay } from "./constants"; import { numPages, numToDisplay } from "./constants";
import { AdminPageTitle } from "@/components/admin/Title"; import { AdminPageTitle } from "@/components/admin/Title";
import { Title } from "@tremor/react";
const Main = () => { const Main = () => {
const { const {
@ -47,10 +48,10 @@ const Main = () => {
return ( return (
<div className="mb-8"> <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} /> <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 <DocumentFeedbackTable
documents={mostDislikedDocuments} documents={mostDislikedDocuments}
refresh={refresh} refresh={refresh}
@ -61,7 +62,7 @@ const Main = () => {
const Page = () => { const Page = () => {
return ( return (
<div> <div className="container mx-auto">
<AdminPageTitle <AdminPageTitle
icon={<ThumbsUpIcon size={32} />} icon={<ThumbsUpIcon size={32} />}
title="Document Feedback" title="Document Feedback"

View File

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

View File

@ -1,6 +1,5 @@
"use client"; "use client";
import { Button } from "@/components/Button";
import { LoadingAnimation, ThreeDotsLoader } from "@/components/Loading"; import { LoadingAnimation, ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector"; import { PageSelector } from "@/components/PageSelector";
import { BasicTable } from "@/components/admin/connectors/BasicTable"; import { BasicTable } from "@/components/admin/connectors/BasicTable";
@ -10,6 +9,17 @@ import {
InfoIcon, InfoIcon,
TrashIcon, TrashIcon,
} from "@/components/icons/icons"; } from "@/components/icons/icons";
import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Title,
Divider,
Badge,
} from "@tremor/react";
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks"; import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
import { ConnectorIndexingStatus, DocumentSet } from "@/lib/types"; import { ConnectorIndexingStatus, DocumentSet } from "@/lib/types";
import { useState } from "react"; import { useState } from "react";
@ -19,7 +29,14 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { deleteDocumentSet } from "./lib"; import { deleteDocumentSet } from "./lib";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup"; import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { AdminPageTitle } from "@/components/admin/Title"; 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; const numToDisplay = 50;
@ -50,15 +67,15 @@ const EditRow = ({
/> />
)} )}
{isSyncingTooltipOpen && ( {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"> <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 text-gray-300" />{" "} <InfoIcon className="mt-1 flex flex-shrink-0 mr-2" /> Cannot update
Cannot update while syncing! Wait for the sync to finish, then try while syncing! Wait for the sync to finish, then try again.
again.
</div> </div>
)} )}
<div <div
className={ 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={() => { onClick={() => {
if (documentSet.is_up_to_date) { 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>
</div> </div>
); );
@ -110,103 +128,101 @@ const DocumentSetTable = ({
return ( return (
<div> <div>
<BasicTable <Title>Existing Document Sets</Title>
columns={[ <Table className="overflow-visible mt-2">
{ <TableHead>
header: "Name", <TableRow>
key: "name", <TableHeaderCell>Name</TableHeaderCell>
}, <TableHeaderCell>Connectors</TableHeaderCell>
{ <TableHeaderCell>Status</TableHeaderCell>
header: "Connectors", <TableHeaderCell>Delete</TableHeaderCell>
key: "ccPairs", </TableRow>
}, </TableHead>
{ <TableBody>
header: "Status", {documentSets
key: "status", .slice((page - 1) * numToDisplay, page * numToDisplay)
}, .map((documentSet) => {
{ return (
header: "Delete", <TableRow key={documentSet.id}>
key: "delete", <TableCell className="whitespace-normal break-all">
width: "50px", <div className="flex gap-x-1 text-emphasis">
}, <EditRow
]} documentSet={documentSet}
data={documentSets ccPairs={ccPairs}
.slice((page - 1) * numToDisplay, page * numToDisplay) setPopup={setPopup}
.map((documentSet) => { refreshDocumentSets={refresh}
return { />
name: ( </div>
<div className="flex gap-x-2"> </TableCell>
<EditRow <TableCell>
documentSet={documentSet} <div>
ccPairs={ccPairs} {documentSet.cc_pair_descriptors.map(
setPopup={setPopup} (ccPairDescriptor, ind) => {
refreshDocumentSets={refresh} return (
/>{" "} <div
<b className="my-auto">{documentSet.name}</b> className={
</div> ind !==
), documentSet.cc_pair_descriptors.length - 1
ccPairs: ( ? "mb-3"
<div> : ""
{documentSet.cc_pair_descriptors.map( }
(ccPairDescriptor, ind) => { key={ccPairDescriptor.id}
return ( >
<div <ConnectorTitle
className={ connector={ccPairDescriptor.connector}
ind !== documentSet.cc_pair_descriptors.length - 1 ccPairName={ccPairDescriptor.name}
? "mb-3" ccPairId={ccPairDescriptor.id}
: "" showMetadata={false}
} />
key={ccPairDescriptor.id} </div>
> );
<ConnectorTitle }
connector={ccPairDescriptor.connector} )}
ccPairName={ccPairDescriptor.name} </div>
ccPairId={ccPairDescriptor.id} </TableCell>
showMetadata={false} <TableCell>
/> {documentSet.is_up_to_date ? (
</div> <Badge size="md" color="green" icon={FiCheckCircle}>
); Up to Date
} </Badge>
)} ) : documentSet.cc_pair_descriptors.length > 0 ? (
</div> <Badge size="md" color="amber" icon={FiClock}>
), Syncing
status: documentSet.is_up_to_date ? ( </Badge>
<div className="text-emerald-600">Up to date!</div> ) : (
) : documentSet.cc_pair_descriptors.length > 0 ? ( <Badge size="md" color="red" icon={FiAlertTriangle}>
<div className="text-gray-300 w-10"> Deleting
<LoadingAnimation text="Syncing" /> </Badge>
</div> )}
) : ( </TableCell>
<div className="text-red-500 w-10"> <TableCell>
<LoadingAnimation text="Deleting" /> <DeleteButton
</div> onClick={async () => {
), const response = await deleteDocumentSet(
delete: ( documentSet.id
<div );
className="cursor-pointer" if (response.ok) {
onClick={async () => { setPopup({
const response = await deleteDocumentSet(documentSet.id); message: `Document set "${documentSet.name}" scheduled for deletion`,
if (response.ok) { type: "success",
setPopup({ });
message: `Document set "${documentSet.name}" scheduled for deletion`, } else {
type: "success", const errorMsg = (await response.json()).detail;
}); setPopup({
} else { message: `Failed to schedule document set for deletion - ${errorMsg}`,
const errorMsg = (await response.json()).detail; type: "error",
setPopup({ });
message: `Failed to schedule document set for deletion - ${errorMsg}`, }
type: "error", refresh();
}); }}
} />
refresh(); </TableCell>
}} </TableRow>
> );
<TrashIcon /> })}
</div> </TableBody>
), </Table>
};
})}
/>
<div className="mt-3 flex"> <div className="mt-3 flex">
<div className="mx-auto"> <div className="mx-auto">
<PageSelector <PageSelector
@ -251,7 +267,7 @@ const Main = () => {
return ( return (
<div className="mb-8"> <div className="mb-8">
{popup} {popup}
<Text className="mb-3 text-gray-300"> <Text className="mb-3">
<b>Document Sets</b> allow you to group logically connected documents <b>Document Sets</b> allow you to group logically connected documents
into a single bundle. These can then be used as filter when performing 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 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. or with a certain command.
</Text> </Text>
<div className="mb-6"></div> <div className="mb-3"></div>
<div className="flex mb-3"> <div className="flex mb-6">
<Button className="ml-2 my-auto" onClick={() => setIsOpen(true)}> <Button
size="xs"
color="green"
className="ml-2 my-auto"
onClick={() => setIsOpen(true)}
>
New Document Set New Document Set
</Button> </Button>
</div> </div>
{documentSets.length > 0 && ( {documentSets.length > 0 && (
<DocumentSetTable <>
documentSets={documentSets} <Divider />
ccPairs={ccPairs} <DocumentSetTable
refresh={refreshDocumentSets} documentSets={documentSets}
setPopup={setPopup} ccPairs={ccPairs}
/> refresh={refreshDocumentSets}
setPopup={setPopup}
/>
</>
)} )}
{isOpen && ( {isOpen && (
@ -292,7 +316,7 @@ const Main = () => {
const Page = () => { const Page = () => {
return ( return (
<div> <div className="container mx-auto">
<AdminPageTitle icon={<BookmarkIcon size={32} />} title="Document Sets" /> <AdminPageTitle icon={<BookmarkIcon size={32} />} title="Document Sets" />
<Main /> <Main />

View File

@ -80,7 +80,7 @@ export function CCPairIndexingStatusTable({
); );
return ( return (
<div className="dark"> <div>
<Table className="overflow-visible"> <Table className="overflow-visible">
<TableHead> <TableHead>
<TableRow> <TableRow>
@ -96,12 +96,12 @@ export function CCPairIndexingStatusTable({
<TableRow <TableRow
key={ccPairsIndexingStatus.cc_pair_id} key={ccPairsIndexingStatus.cc_pair_id}
className={ 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> <TableCell>
<div className="flex my-auto"> <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"> <div className="whitespace-normal break-all max-w-3xl">
<ConnectorTitle <ConnectorTitle
connector={ccPairsIndexingStatus.connector} connector={ccPairsIndexingStatus.connector}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,3 +1,5 @@
import { Prompt } from "./interfaces";
interface PersonaCreationRequest { interface PersonaCreationRequest {
name: string; name: string;
description: string; description: string;
@ -5,41 +7,157 @@ interface PersonaCreationRequest {
task_prompt: string; task_prompt: string;
document_set_ids: number[]; document_set_ids: number[];
num_chunks: number | null; num_chunks: number | null;
apply_llm_relevance_filter: boolean | null; 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),
});
} }
interface PersonaUpdateRequest { interface PersonaUpdateRequest {
id: number; id: number;
existingPromptId: number;
name: string;
description: string; description: string;
system_prompt: string; system_prompt: string;
task_prompt: string; task_prompt: string;
document_set_ids: number[]; document_set_ids: number[];
num_chunks: number | null; num_chunks: number | null;
apply_llm_relevance_filter: boolean | null; llm_relevance_filter: boolean | null;
} }
export function updatePersona(personaUpdateRequest: PersonaUpdateRequest) { function promptNameFromPersonaName(personaName: string) {
const { id, ...requestBody } = personaUpdateRequest; 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", method: "PATCH",
headers: { headers: {
"Content-Type": "application/json", "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) { export function deletePersona(personaId: number) {
return fetch(`/api/admin/persona/${personaId}`, { return fetch(`/api/admin/persona/${personaId}`, {
method: "DELETE", method: "DELETE",

View File

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

View File

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

View File

@ -1,101 +1,112 @@
"use client"; "use client";
import { Button } from "@/components/Button"; import {
Table,
TableHead,
TableRow,
TableHeaderCell,
TableBody,
TableCell,
Button,
} from "@tremor/react";
import { LoadingAnimation } from "@/components/Loading"; import { LoadingAnimation } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title"; import { AdminPageTitle } from "@/components/admin/Title";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { usePopup } from "@/components/admin/connectors/Popup"; import { usePopup } from "@/components/admin/connectors/Popup";
import { UsersIcon } from "@/components/icons/icons"; import { UsersIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher"; import { fetcher } from "@/lib/fetcher";
import { User } from "@/lib/types"; import { User } from "@/lib/types";
import useSWR, { mutate } from "swr"; import useSWR, { mutate } from "swr";
const columns = [
{
header: "Email",
key: "email",
},
{
header: "Role",
key: "role",
},
{
header: "Promote",
key: "promote",
},
];
const UsersTable = () => { const UsersTable = () => {
const { popup, setPopup } = usePopup(); const { popup, setPopup } = usePopup();
const { data, isLoading, error } = useSWR<User[]>( const {
"/api/manage/users", data: users,
fetcher isLoading,
); error,
} = useSWR<User[]>("/api/manage/users", fetcher);
if (isLoading) { if (isLoading) {
return <LoadingAnimation text="Loading" />; return <LoadingAnimation text="Loading" />;
} }
if (error || !data) { if (error || !users) {
return <div className="text-red-600">Error loading users</div>; return <div className="text-error">Error loading users</div>;
} }
return ( return (
<div> <div>
{popup} {popup}
<BasicTable
columns={columns} <Table className="overflow-visible">
data={data.map((user) => { <TableHead>
return { <TableRow>
email: user.email, <TableHeaderCell>Email</TableHeaderCell>
role: <i>{user.role === "admin" ? "Admin" : "User"}</i>, <TableHeaderCell>Role</TableHeaderCell>
promote: <TableHeaderCell>
user.role !== "admin" ? ( <div className="flex">
<Button <div className="ml-auto">Promote</div>
onClick={async () => { </div>
const res = await fetch( </TableHeaderCell>
"/api/manage/promote-user-to-admin", </TableRow>
{ </TableHead>
method: "PATCH", <TableBody>
headers: { {users.map((user) => {
"Content-Type": "application/json", return (
}, <TableRow key={user.id}>
body: JSON.stringify({ <TableCell>{user.email}</TableCell>
user_email: user.email, <TableCell>
}), <i>{user.role === "admin" ? "Admin" : "User"}</i>
} </TableCell>
); <TableCell>
if (!res.ok) { <div className="flex">
const errorMsg = await res.text(); <div className="ml-auto">
setPopup({ <Button
message: `Unable to promote user - ${errorMsg}`, onClick={async () => {
type: "error", const res = await fetch(
}); "/api/manage/promote-user-to-admin",
} else { {
mutate("/api/manage/users"); method: "PATCH",
setPopup({ headers: {
message: "User promoted to admin!", "Content-Type": "application/json",
type: "success", },
}); body: JSON.stringify({
} user_email: user.email,
}} }),
> }
Promote to Admin! );
</Button> if (!res.ok) {
) : ( const errorMsg = await res.text();
"" setPopup({
), message: `Unable to promote user - ${errorMsg}`,
}; type: "error",
})} });
/> } else {
mutate("/api/manage/users");
setPopup({
message: "User promoted to admin!",
type: "success",
});
}
}}
>
Promote to Admin!
</Button>
</div>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div> </div>
); );
}; };
const Page = () => { const Page = () => {
return ( return (
<div> <div className="mx-auto container">
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} /> <AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
<UsersTable /> <UsersTable />

View File

@ -42,7 +42,7 @@ export function SignInButton({
return ( return (
<a <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} href={authorizeUrl}
> >
{button} {button}

View File

@ -77,7 +77,7 @@ const Page = async ({
<div className="h-16 w-16 mx-auto"> <div className="h-16 w-16 mx-auto">
<Image src="/logo.png" alt="Logo" width="1419" height="1520" /> <Image src="/logo.png" alt="Logo" width="1419" height="1520" />
</div> </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 Log In to Danswer
</h2> </h2>
{authUrl && authTypeMetadata && ( {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 base;
@tailwind components; @tailwind components;
@tailwind utilities; @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 ( return (
<html lang="en"> <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} {children}
</body> </body>
</html> </html>

View File

@ -8,7 +8,7 @@ import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, User } from "@/lib/types"; import { Connector, DocumentSet, User } from "@/lib/types";
import { cookies } from "next/headers"; import { cookies } from "next/headers";
import { SearchType } from "@/lib/search/interfaces"; import { SearchType } from "@/lib/search/interfaces";
import { Persona } from "./admin/personas/interfaces"; import { Persona } from "../admin/personas/interfaces";
import { WelcomeModal } from "@/components/WelcomeModal"; import { WelcomeModal } from "@/components/WelcomeModal";
import { unstable_noStore as noStore } from "next/cache"; import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh"; import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
@ -88,7 +88,7 @@ export default async function Home() {
<ApiKeyModal /> <ApiKeyModal />
<InstantSSRAutoRefresh /> <InstantSSRAutoRefresh />
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />} {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"> <div className="w-full">
<SearchSection <SearchSection
connectors={connectors} connectors={connectors}

View File

@ -13,7 +13,7 @@ export function BackButton() {
my-auto my-auto
flex flex
mb-1 mb-1
hover:bg-gray-800 hover:bg-hover-light
w-fit w-fit
p-1 p-1
pr-2 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="relative">
<span <span
className={`block w-3 h-3 border border-gray-600 rounded ${ className={`block w-3 h-3 border border-border-strong rounded ${
checked ? "bg-green-700" : "bg-gray-800" checked ? "bg-green-700" : "bg-background"
} transition duration-300`} } transition duration-300`}
> >
{checked && ( {checked && (
<svg <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" viewBox="0 0 20 20"
> >
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z" /> <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 && ( {isOpen && (
<div <div
onClick={() => setIsOpen(!isOpen)} 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} {dropdown}
</div> </div>
@ -323,48 +323,52 @@ export const CustomDropdown = ({
); );
}; };
function DefaultDropdownElement({ export function DefaultDropdownElement({
id,
name, name,
icon,
description, description,
onSelect, onSelect,
isSelected, isSelected,
isFinal, includeCheckbox = false,
}: { }: {
id: string | number | null;
name: string; name: string;
icon?: React.FC<{ size?: number; className?: string }>;
description?: string; description?: string;
onSelect: (value: string | number | null) => void; onSelect?: () => void;
isSelected: boolean; isSelected?: boolean;
isFinal: boolean; includeCheckbox?: boolean;
}) { }) {
console.log(isFinal);
return ( return (
<div <div
className={` className={`
flex flex
px-3 mx-1
px-2
text-sm text-sm
text-gray-200 py-1.5
py-2.5 my-1
select-none select-none
cursor-pointer cursor-pointer
${isFinal ? "" : "border-b border-gray-800"} bg-background
${ rounded
isSelected hover:bg-hover-light
? "bg-dark-tremor-background-muted"
: "hover:bg-dark-tremor-background-muted "
}
`} `}
onClick={() => { onClick={onSelect}
onSelect(id);
}}
> >
<div> <div>
{name} <div className="flex">
{description && ( {includeCheckbox && (
<div className="text-xs text-dark-tremor-content">{description}</div> <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> </div>
{isSelected && ( {isSelected && (
<div className="ml-auto mr-1 my-auto"> <div className="ml-auto mr-1 my-auto">
@ -394,10 +398,11 @@ export function DefaultDropdown({
<div <div
className={` className={`
border border
border-gray-800 border
rounded-lg rounded-lg
flex flex
flex-col flex-col
bg-background
max-h-96 max-h-96
overflow-y-auto overflow-y-auto
overscroll-contain`} overscroll-contain`}
@ -405,13 +410,11 @@ export function DefaultDropdown({
{includeDefault && ( {includeDefault && (
<DefaultDropdownElement <DefaultDropdownElement
key={-1} key={-1}
id={null}
name="Default" name="Default"
onSelect={() => { onSelect={() => {
onSelect(null); onSelect(null);
}} }}
isSelected={selected === null} isSelected={selected === null}
isFinal={false}
/> />
)} )}
{options.map((option, ind) => { {options.map((option, ind) => {
@ -419,12 +422,10 @@ export function DefaultDropdown({
return ( return (
<DefaultDropdownElement <DefaultDropdownElement
key={option.value} key={option.value}
id={option.value}
name={option.name} name={option.name}
description={option.description} description={option.description}
onSelect={onSelect} onSelect={() => onSelect(option.value)}
isSelected={isSelected} isSelected={isSelected}
isFinal={ind === options.length - 1}
/> />
); );
})} })}
@ -435,16 +436,15 @@ export function DefaultDropdown({
className={` className={`
flex flex
text-sm text-sm
text-gray-400 bg-background
px-3 px-3
py-1.5 py-1.5
rounded-lg rounded-lg
border border
border-gray-800 border-border
cursor-pointer cursor-pointer`}
hover:bg-dark-tremor-background-muted`}
> >
<p className="text-gray-200 line-clamp-1"> <p className="line-clamp-1">
{selectedOption?.name || {selectedOption?.name ||
(includeDefault ? "Default" : "Select an option...")} (includeDefault ? "Default" : "Select an option...")}
</p> </p>
@ -453,3 +453,45 @@ export function DefaultDropdown({
</CustomDropdown> </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"; "use client";
import { useRouter } from "next/navigation"; import { FiEdit } from "react-icons/fi";
import { FiChevronLeft, FiEdit } from "react-icons/fi";
export function EditButton({ onClick }: { onClick: () => void }) { export function EditButton({ onClick }: { onClick: () => void }) {
return ( return (
@ -11,12 +9,12 @@ export function EditButton({ onClick }: { onClick: () => void }) {
my-auto my-auto
flex flex
mb-1 mb-1
hover:bg-gray-800 hover:bg-hover
w-fit w-fit
p-2 p-2
cursor-pointer cursor-pointer
rounded-lg rounded-lg
border-gray-800 border-border
text-sm`} text-sm`}
onClick={onClick} onClick={onClick}
> >

View File

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

View File

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

View File

@ -22,14 +22,14 @@ export function Modal({
> >
<div <div
className={` className={`
bg-gray-800 rounded-sm shadow-lg bg-background rounded-sm shadow-lg
shadow-lg relative w-1/2 text-sm shadow-lg relative w-1/2 text-sm
${className} ${className}
`} `}
onClick={(event) => event.stopPropagation()} onClick={(event) => event.stopPropagation()}
> >
{title && ( {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} {title}
</h2> </h2>
)} )}

View File

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

View File

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

View File

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

View File

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

View File

@ -13,12 +13,12 @@ export function AdminPageTitle({
includeDivider?: boolean; includeDivider?: boolean;
}) { }) {
return ( return (
<div className="dark"> <div>
<div className="mb-4"> <div className="mb-4">
<HealthCheckBanner /> <HealthCheckBanner />
</div> </div>
<div className="flex"> <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} {icon} {title}
</h1> </h1>
{farRightElement && <div className="ml-auto">{farRightElement}</div>} {farRightElement && <div className="ml-auto">{farRightElement}</div>}

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