mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-06-30 01:30:45 +02:00
Chat UI
This commit is contained in:
@ -9,9 +9,9 @@ class CreatePromptRequest(BaseModel):
|
||||
shared: bool
|
||||
system_prompt: str
|
||||
task_prompt: str
|
||||
include_citations: bool
|
||||
datetime_aware: bool
|
||||
persona_ids: list[int]
|
||||
include_citations: bool = False
|
||||
datetime_aware: bool = False
|
||||
persona_ids: list[int] | None = None
|
||||
|
||||
|
||||
class PromptSnapshot(BaseModel):
|
||||
|
@ -59,6 +59,7 @@ def get_user_chat_sessions(
|
||||
ChatSessionDetails(
|
||||
id=chat.id,
|
||||
name=chat.description,
|
||||
persona_id=chat.persona_id,
|
||||
time_created=chat.time_created.isoformat(),
|
||||
)
|
||||
for chat in chat_sessions
|
||||
|
@ -96,6 +96,7 @@ class RenameChatSessionResponse(BaseModel):
|
||||
class ChatSessionDetails(BaseModel):
|
||||
id: int
|
||||
name: str
|
||||
persona_id: int
|
||||
time_created: str
|
||||
|
||||
|
||||
|
@ -24,20 +24,35 @@ const nextConfig = {
|
||||
// In production, something else (nginx in the one box setup) should take
|
||||
// care of this redirect. TODO (chris): better support setups where
|
||||
// web_server and api_server are on different machines.
|
||||
if (process.env.NODE_ENV === "production") return [];
|
||||
|
||||
return [
|
||||
const defaultRedirects = [
|
||||
{
|
||||
source: "/api/stream-direct-qa:params*",
|
||||
destination: "http://127.0.0.1:8080/stream-direct-qa:params*", // Proxy to Backend
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/api/stream-query-validation:params*",
|
||||
destination: "http://127.0.0.1:8080/stream-query-validation:params*", // Proxy to Backend
|
||||
source: "/",
|
||||
destination: "/search",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NODE_ENV === "production") return defaultRedirects;
|
||||
|
||||
return defaultRedirects.concat([
|
||||
{
|
||||
source: "/api/chat/send-message:params*",
|
||||
destination: "http://127.0.0.1:8080/chat/send-message:params*", // Proxy to Backend
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/api/query/stream-answer-with-quote:params*",
|
||||
destination:
|
||||
"http://127.0.0.1:8080/query/stream-answer-with-quote:params*", // Proxy to Backend
|
||||
permanent: true,
|
||||
},
|
||||
{
|
||||
source: "/api/query/stream-query-validation:params*",
|
||||
destination:
|
||||
"http://127.0.0.1:8080/query/stream-query-validation:params*", // Proxy to Backend
|
||||
permanent: true,
|
||||
},
|
||||
]);
|
||||
},
|
||||
publicRuntimeConfig: {
|
||||
version,
|
||||
|
Binary file not shown.
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 6.2 KiB |
BIN
web/public/GithubDarkMode.png
Normal file
BIN
web/public/GithubDarkMode.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.7 KiB |
@ -13,20 +13,20 @@ function SourceTile({ sourceMetadata }: { sourceMetadata: SourceMetadata }) {
|
||||
flex-col
|
||||
items-center
|
||||
justify-center
|
||||
bg-dark-tremor-background-muted
|
||||
p-4
|
||||
rounded-lg
|
||||
w-40
|
||||
cursor-pointer
|
||||
bg-hover-light
|
||||
shadow-md
|
||||
hover:bg-gray-800
|
||||
hover:bg-hover
|
||||
`}
|
||||
href={sourceMetadata.adminUrl}
|
||||
>
|
||||
<SourceIcon sourceType={sourceMetadata.internalName} iconSize={24} />
|
||||
<span className="font-medium text-sm text-gray-300 mt-2">
|
||||
<Text className="font-medium text-sm mt-2">
|
||||
{sourceMetadata.displayName}
|
||||
</span>
|
||||
</Text>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
@ -42,26 +42,26 @@ export default function Page() {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="mx-auto container dark">
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle
|
||||
icon={<ConnectorIcon size={32} />}
|
||||
title="Add Connector"
|
||||
/>
|
||||
|
||||
<div className="text-gray-300 text-sm">
|
||||
<Text>
|
||||
Connect Danswer to your organization's knowledge sources.
|
||||
We'll automatically sync your data into Danswer, so you can find
|
||||
exactly what you're looking for in one place.
|
||||
</div>
|
||||
</Text>
|
||||
|
||||
<div className="flex mt-8">
|
||||
<Title>Import Knowledge</Title>
|
||||
</div>
|
||||
<div className="text-gray-300 text-sm">
|
||||
<Text>
|
||||
Connect to pieces of knowledge that live outside your apps. Upload
|
||||
files, scrape websites, or connect to your organization's Google
|
||||
Site.
|
||||
</div>
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-4 p-4">
|
||||
{importedKnowledgeSources.map((source) => {
|
||||
return (
|
||||
@ -73,11 +73,11 @@ export default function Page() {
|
||||
<div className="flex mt-8">
|
||||
<Title>Setup Auto-Syncing from Apps</Title>
|
||||
</div>
|
||||
<div className="text-gray-300 text-sm">
|
||||
<Text>
|
||||
Setup auto-syncing from your organization's most used apps and
|
||||
services. Unless otherwise specified during the connector setup, we will
|
||||
pull in the latest updates from the source every 10 minutes.
|
||||
</div>
|
||||
</Text>
|
||||
<div className="flex flex-wrap gap-4 p-4">
|
||||
{appConnectionSources.map((source) => {
|
||||
return (
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
updateSlackBotConfig,
|
||||
} from "./lib";
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
Divider,
|
||||
Tab,
|
||||
@ -52,7 +53,7 @@ export const SlackBotCreationForm = ({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
<Card>
|
||||
{popup}
|
||||
<Formik
|
||||
@ -193,7 +194,7 @@ export const SlackBotCreationForm = ({
|
||||
Use either a Persona <b>or</b> Document Sets to control how
|
||||
DanswerBot answers.
|
||||
</Text>
|
||||
<div className="text-dark-tremor-content text-sm">
|
||||
<Text>
|
||||
<ul className="list-disc mt-2 ml-4">
|
||||
<li>
|
||||
You should use a Persona if you also want to customize
|
||||
@ -204,7 +205,7 @@ export const SlackBotCreationForm = ({
|
||||
which documents DanswerBot uses as references.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Text>
|
||||
<Text className="mt-2">
|
||||
<b>NOTE:</b> whichever tab you are when you submit the form
|
||||
will be the one that is used. For example, if you are on the
|
||||
@ -249,13 +250,13 @@ export const SlackBotCreationForm = ({
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-gray-700
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " bg-gray-900 hover:bg-gray-700")
|
||||
? " bg-hover"
|
||||
: " bg-background hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
@ -289,7 +290,6 @@ export const SlackBotCreationForm = ({
|
||||
value: persona.id,
|
||||
};
|
||||
})}
|
||||
includeDefault={true}
|
||||
/>
|
||||
</TabPanel>
|
||||
</TabPanels>
|
||||
@ -298,17 +298,13 @@ export const SlackBotCreationForm = ({
|
||||
<Divider />
|
||||
|
||||
<div className="flex">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={
|
||||
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||
"focus:shadow-outline w-full max-w-sm mx-auto"
|
||||
}
|
||||
className="mx-auto w-64"
|
||||
>
|
||||
{isUpdate ? "Update!" : "Create!"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
|
@ -11,7 +11,7 @@ import {
|
||||
setSlackBotTokens,
|
||||
updateSlackBotConfig,
|
||||
} from "./lib";
|
||||
import { Card } from "@tremor/react";
|
||||
import { Button, Card } from "@tremor/react";
|
||||
|
||||
interface SlackBotTokensFormProps {
|
||||
onClose: () => void;
|
||||
@ -64,17 +64,9 @@ export const SlackBotTokensForm = ({
|
||||
type="password"
|
||||
/>
|
||||
<div className="flex">
|
||||
<button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={
|
||||
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||
"focus:shadow-outline w-full max-w-sm mx-auto"
|
||||
}
|
||||
>
|
||||
<Button type="submit" disabled={isSubmitting}>
|
||||
Set Tokens
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
|
@ -62,7 +62,7 @@ async function Page({ params }: { params: { id: string } }) {
|
||||
const personas = (await personasResponse.json()) as Persona[];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto dark">
|
||||
<div className="container mx-auto">
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<BackButton />
|
||||
|
@ -33,7 +33,7 @@ async function Page() {
|
||||
const personas = (await personasResponse.json()) as Persona[];
|
||||
|
||||
return (
|
||||
<div className="container mx-auto dark">
|
||||
<div className="container mx-auto">
|
||||
<BackButton />
|
||||
<AdminPageTitle
|
||||
icon={<CPUIcon size={32} />}
|
||||
|
@ -173,7 +173,7 @@ const Main = () => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="mb-8 dark">
|
||||
<div className="mb-8">
|
||||
{popup}
|
||||
|
||||
<Text className="mb-2">
|
||||
@ -181,7 +181,7 @@ const Main = () => {
|
||||
to ask questions to Danswer directly from Slack. Additionally, you can:
|
||||
</Text>
|
||||
|
||||
<div className="text-dark-tremor-content text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
<ul className="list-disc mt-2 ml-4">
|
||||
<li>
|
||||
Setup DanswerBot to automatically answer questions in certain
|
||||
@ -196,7 +196,7 @@ const Main = () => {
|
||||
UI.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</Text>
|
||||
|
||||
<Text className="mb-6">
|
||||
Follow the{" "}
|
||||
@ -226,7 +226,7 @@ const Main = () => {
|
||||
setSlackBotTokensModalIsOpen(!slackBotTokensModalIsOpen);
|
||||
console.log(slackBotTokensModalIsOpen);
|
||||
}}
|
||||
variant="secondary"
|
||||
color="blue"
|
||||
size="xs"
|
||||
className="mt-2"
|
||||
icon={slackBotTokensModalIsOpen ? FiChevronUp : FiChevronDown}
|
||||
@ -259,7 +259,7 @@ const Main = () => {
|
||||
<div className="mb-2"></div>
|
||||
|
||||
<Link className="flex mb-3" href="/admin/bot/new">
|
||||
<Button className="my-auto" variant="secondary" size="xs">
|
||||
<Button className="my-auto" color="green" size="xs">
|
||||
New Slack Bot Configuration
|
||||
</Button>
|
||||
</Link>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { getNameFromPath } from "@/lib/fileUtils";
|
||||
import { ValidSources } from "@/lib/types";
|
||||
import { List, ListItem, Card, Title, Divider } from "@tremor/react";
|
||||
import { List, ListItem, Card, Title } from "@tremor/react";
|
||||
|
||||
function convertObjectToString(obj: any): string | any {
|
||||
// Check if obj is an object and not an array or null
|
||||
|
@ -30,7 +30,6 @@ export function DeletionButton({ ccPair }: { ccPair: CCPairFullInfo }) {
|
||||
<div>
|
||||
{popup}
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
color="red"
|
||||
onClick={() =>
|
||||
|
@ -19,7 +19,7 @@ export function ModifyStatusButtonCluster({
|
||||
{popup}
|
||||
{ccPair.connector.disabled ? (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="green"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
disableConnector(ccPair.connector, setPopup, () => router.refresh())
|
||||
@ -30,7 +30,7 @@ export function ModifyStatusButtonCluster({
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
variant="secondary"
|
||||
color="red"
|
||||
size="xs"
|
||||
onClick={() =>
|
||||
disableConnector(ccPair.connector, setPopup, () => router.refresh())
|
||||
|
@ -22,7 +22,7 @@ export function ReIndexButton({
|
||||
{popup}
|
||||
<Button
|
||||
className="ml-auto"
|
||||
variant="secondary"
|
||||
color="green"
|
||||
size="xs"
|
||||
onClick={async () => {
|
||||
const errorMsg = await runConnector(connectorId, [credentialId]);
|
||||
|
@ -56,14 +56,14 @@ export default async function Page({
|
||||
return (
|
||||
<>
|
||||
<SSRAutoRefresh />
|
||||
<div className="mx-auto container dark">
|
||||
<div className="mx-auto container">
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
|
||||
<BackButton />
|
||||
<div className="pb-1 flex mt-1">
|
||||
<h1 className="text-3xl font-bold">{ccPair.name}</h1>
|
||||
<h1 className="text-3xl text-emphasis font-bold">{ccPair.name}</h1>
|
||||
|
||||
<div className="ml-auto">
|
||||
<ModifyStatusButtonCluster ccPair={ccPair} />
|
||||
@ -76,9 +76,9 @@ export default async function Page({
|
||||
isDeleting={isDeleting}
|
||||
/>
|
||||
|
||||
<div className="text-gray-400 text-sm mt-1">
|
||||
<div className="text-sm mt-1">
|
||||
Total Documents Indexed:{" "}
|
||||
<b className="text-gray-300">{totalDocsIndexed}</b>
|
||||
<b className="text-emphasis">{totalDocsIndexed}</b>
|
||||
</div>
|
||||
|
||||
<Divider />
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -69,19 +71,19 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your API details
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{bookstackCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing API Token: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md">
|
||||
<Text className="my-auto">Existing API Token: </Text>
|
||||
<Text className="ml-1 italic my-auto max-w-md">
|
||||
{bookstackCredential.credential_json?.bookstack_api_token_id}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (bookstackConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -101,15 +103,15 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To get started you'll need API token details for your BookStack
|
||||
instance. You can get these by editing your (or another) user
|
||||
account in BookStack and creating a token via the 'API
|
||||
Tokens' section at the bottom. Your user account will require
|
||||
to be assigned a BookStack role which has the 'Access system
|
||||
API' system permission assigned.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
|
||||
</Text>
|
||||
<Card className="mt-2 mb-4">
|
||||
<CredentialForm<BookstackCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -151,19 +153,19 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{bookstackConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
BookStack indexing status
|
||||
</h2>
|
||||
<p className="text-sm mb-2">
|
||||
</Title>
|
||||
<Text className="mb-2">
|
||||
The latest page, chapter, book and shelf changes are fetched every
|
||||
10 minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<BookstackConfig, BookstackCredentialJson>
|
||||
connectorIndexingStatuses={bookstackConnectorIndexingStatuses}
|
||||
@ -192,12 +194,12 @@ const Main = () => {
|
||||
{bookstackCredential &&
|
||||
bookstackConnectorIndexingStatuses.length === 0 && (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Create Connection</h2>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
Press connect below to start the connection to your BookStack
|
||||
instance.
|
||||
</p>
|
||||
</Text>
|
||||
<ConnectorForm<BookstackConfig>
|
||||
nameBuilder={(values) => `BookStackConnector`}
|
||||
ccPairNameBuilder={(values) => `BookStackConnector`}
|
||||
@ -209,17 +211,17 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={bookstackCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!bookstackCredential && (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
Please provide your API details in Step 1 first! Once done with
|
||||
that, you'll be able to start the connection then see indexing
|
||||
status.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -232,10 +234,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<BookstackIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">BookStack</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<BookstackIcon size={32} />} title="Bookstack" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const extractSpaceFromCloudUrl = (wikiUrl: string): string => {
|
||||
const parsedUrl = new URL(wikiUrl);
|
||||
@ -100,9 +102,9 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your access token
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{confluenceCredential ? (
|
||||
<>
|
||||
@ -118,7 +120,7 @@ const Main = () => {
|
||||
{confluenceCredential.credential_json?.confluence_access_token}
|
||||
</p>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (confluenceConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -138,17 +140,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the Confluence connector, first follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://docs.danswer.dev/connectors/confluence#setting-up"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
to generate an Access Token.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<ConfluenceCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -178,7 +181,7 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -254,10 +257,11 @@ const Main = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Add a New Space</h2>
|
||||
<ConnectorForm<ConfluenceConfig>
|
||||
nameBuilder={(values) =>
|
||||
@ -284,14 +288,14 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={confluenceCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with that,
|
||||
you can then specify which Confluence spaces you want to make
|
||||
searchable.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -303,10 +307,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<ConfluenceIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Confluence</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<ConfluenceIcon size={32} />} title="Confluence" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Title, Text, Card, Divider } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const MainSection = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
@ -71,18 +73,18 @@ const MainSection = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
{document360Credential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Document360 API Token: </p>
|
||||
<p className="ml-1 italic my-auto">
|
||||
<Text className="my-auto">Existing Document360 API Token: </Text>
|
||||
<Text className="ml-1 italic my-auto">
|
||||
{document360Credential.credential_json.document360_api_token}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
await adminDeleteCredential(document360Credential.id);
|
||||
refreshCredentials();
|
||||
@ -94,19 +96,20 @@ const MainSection = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
To use the Document360 connector, you must first provide the API
|
||||
token and portal ID corresponding to your Document360 setup. See
|
||||
setup guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://docs.danswer.dev/connectors/document360"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
for more detail.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-2">
|
||||
<CredentialForm<Document360CredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -134,20 +137,20 @@ const MainSection = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which categories do you want to make searchable?
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{document360ConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We index the latest articles from each workspace listed below
|
||||
regularly.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<Document360Config, Document360CredentialJson>
|
||||
connectorIndexingStatuses={document360ConnectorIndexingStatuses}
|
||||
@ -187,11 +190,12 @@ const MainSection = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{document360Credential ? (
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Connect to a New Workspace</h2>
|
||||
<ConnectorForm<Document360Config>
|
||||
nameBuilder={(values) =>
|
||||
@ -232,13 +236,13 @@ const MainSection = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={document360Credential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your Document360 API token and portal ID in Step 1
|
||||
first! Once done with that, you can then specify which Document360
|
||||
categories you want to make searchable.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -250,10 +254,12 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<Document360Icon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Document360</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<Document360Icon size={32} />}
|
||||
title="Document360"
|
||||
/>
|
||||
|
||||
<MainSection />
|
||||
</div>
|
||||
);
|
||||
|
@ -18,6 +18,8 @@ import { Form, Formik } from "formik";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { FileUpload } from "@/components/admin/connectors/FileUpload";
|
||||
import { getNameFromPath } from "@/lib/fileUtils";
|
||||
import { Button, Card, Divider, Text } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const Main = () => {
|
||||
const [selectedFiles, setSelectedFiles] = useState<File[]>([]);
|
||||
@ -48,13 +50,13 @@ const Main = () => {
|
||||
<div>
|
||||
{popup}
|
||||
{filesAreUploading && <Spinner />}
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
Specify files below, click the <b>Upload</b> button, and the contents of
|
||||
these files will be searchable via Danswer! Currently only <i>.txt</i>,{" "}
|
||||
<i>.pdf</i> and <i>.zip</i> files (containing only <i>.txt</i> files)
|
||||
are supported.
|
||||
</p>
|
||||
<div className="text-sm mb-3">
|
||||
</Text>
|
||||
<Text className="mb-3">
|
||||
<b>NOTE:</b> if the original document is accessible via a link, you can
|
||||
add a line at the very beginning of the file that looks like:
|
||||
<div className="flex my-2">
|
||||
@ -67,13 +69,14 @@ const Main = () => {
|
||||
search result. More details on this can be found in the{" "}
|
||||
<a
|
||||
href="https://docs.danswer.dev/connectors/file"
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
>
|
||||
documentation.
|
||||
</a>
|
||||
</div>
|
||||
</Text>
|
||||
<div className="flex mt-4">
|
||||
<div className="mx-auto max-w-3xl w-full">
|
||||
<div className="mx-auto w-full">
|
||||
<Card>
|
||||
<Formik
|
||||
initialValues={{
|
||||
name: "",
|
||||
@ -142,7 +145,8 @@ const Main = () => {
|
||||
formikHelpers.setSubmitting(false);
|
||||
return;
|
||||
}
|
||||
const credentialId = (await createCredentialResponse.json()).id;
|
||||
const credentialId = (await createCredentialResponse.json())
|
||||
.id;
|
||||
|
||||
const credentialResponse = await linkCredential(
|
||||
connector.id,
|
||||
@ -159,9 +163,10 @@ const Main = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const runConnectorErrorMsg = await runConnector(connector.id, [
|
||||
0,
|
||||
]);
|
||||
const runConnectorErrorMsg = await runConnector(
|
||||
connector.id,
|
||||
[0]
|
||||
);
|
||||
if (runConnectorErrorMsg) {
|
||||
setPopup({
|
||||
message: `Unable to run connector - ${runConnectorErrorMsg}`,
|
||||
@ -189,8 +194,10 @@ const Main = () => {
|
||||
}}
|
||||
>
|
||||
{({ values, isSubmitting }) => (
|
||||
<Form className="p-3 border border-gray-600 rounded">
|
||||
<h2 className="font-bold text-xl mb-2">Upload Files</h2>
|
||||
<Form>
|
||||
<h2 className="font-bold text-emphasis text-xl mb-2">
|
||||
Upload Files
|
||||
</h2>
|
||||
<TextFormField
|
||||
name="name"
|
||||
label="Name:"
|
||||
@ -198,32 +205,36 @@ const Main = () => {
|
||||
autoCompleteDisabled={true}
|
||||
/>
|
||||
|
||||
<p className="mb-1">Files:</p>
|
||||
<p className="mb-1 font-medium text-emphasis">Files:</p>
|
||||
<FileUpload
|
||||
selectedFiles={selectedFiles}
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
/>
|
||||
<button
|
||||
className={
|
||||
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||
"focus:shadow-outline w-full mx-auto mt-4"
|
||||
}
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="mt-4 w-64 mx-auto"
|
||||
color="green"
|
||||
size="xs"
|
||||
type="submit"
|
||||
disabled={
|
||||
selectedFiles.length === 0 || !values.name || isSubmitting
|
||||
selectedFiles.length === 0 ||
|
||||
!values.name ||
|
||||
isSubmitting
|
||||
}
|
||||
>
|
||||
Upload!
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{fileIndexingStatuses.length > 0 && (
|
||||
<div className="mt-6">
|
||||
<div>
|
||||
<Divider />
|
||||
<h2 className="font-bold text-xl mb-2">Indexed Files</h2>
|
||||
<SingleUseConnectorsTable<FileConfig, {}>
|
||||
connectorIndexingStatuses={fileIndexingStatuses}
|
||||
@ -253,10 +264,9 @@ export default function File() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
||||
<FileIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">File</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<FileIcon size={32} />} title="File" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -18,6 +18,8 @@ import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||
import { adminDeleteCredential, linkCredential } from "@/lib/credential";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const Main = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
@ -66,9 +68,9 @@ const Main = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your access token
|
||||
</h2>
|
||||
</Title>
|
||||
{githubCredential ? (
|
||||
<>
|
||||
{" "}
|
||||
@ -78,7 +80,7 @@ const Main = () => {
|
||||
{githubCredential.credential_json.github_access_token}
|
||||
</p>{" "}
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
await adminDeleteCredential(githubCredential.id);
|
||||
refreshCredentials();
|
||||
@ -90,17 +92,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
If you don't have an access token, read the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href="https://docs.danswer.dev/connectors/github"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
on how to get one from Github.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<GithubCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -125,20 +128,20 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which repositories do you want to make searchable?
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{githubConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We pull the latest Pull Requests from each repository listed below
|
||||
every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<GithubConfig, GithubCredentialJson>
|
||||
connectorIndexingStatuses={githubConnectorIndexingStatuses}
|
||||
@ -168,11 +171,12 @@ const Main = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{githubCredential ? (
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Connect to a New Repository</h2>
|
||||
<ConnectorForm<GithubConfig>
|
||||
nameBuilder={(values) =>
|
||||
@ -208,13 +212,13 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={githubCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with that,
|
||||
you can then specify which Github repositories you want to make
|
||||
searchable.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -226,10 +230,12 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<GithubIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Github PRs</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<GithubIcon size={32} />}
|
||||
title="Github PRs + Issues"
|
||||
/>
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -22,6 +22,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -75,21 +77,21 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
This connector allows you to sync all your Gong Transcripts into
|
||||
Danswer. More details on how to setup the Gong connector can be found in{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://docs.danswer.dev/connectors/gong"
|
||||
target="_blank"
|
||||
>
|
||||
this guide.
|
||||
</a>
|
||||
</p>
|
||||
</Text>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your API Access info
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{gongCredential ? (
|
||||
<>
|
||||
@ -99,7 +101,7 @@ const Main = () => {
|
||||
{gongCredential.credential_json?.gong_access_key_secret}
|
||||
</p>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (gongConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -119,7 +121,7 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<GongCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -149,19 +151,19 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which Workspaces do you want to make searchable?
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{gongConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We pull the latest transcript every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<GongConfig, GongCredentialJson>
|
||||
connectorIndexingStatuses={gongConnectorIndexingStatuses}
|
||||
@ -196,12 +198,13 @@ const Main = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{gongCredential ? (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Create a new Gong Connector</h2>
|
||||
<ConnectorForm<GongConfig>
|
||||
nameBuilder={(values) =>
|
||||
@ -230,13 +233,13 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={gongCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your API Access Info in Step 1 first! Once done with
|
||||
that, you can then start indexing all your Gong transcripts.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -248,10 +251,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<GongIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Gong</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<GongIcon size={32} />} title="Gong" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -7,6 +7,8 @@ import { XIcon } from "@/components/icons/icons";
|
||||
import { Connector, GoogleDriveConfig } from "@/lib/types";
|
||||
import * as Yup from "yup";
|
||||
import { googleDriveConnectorNameBuilder } from "./utils";
|
||||
import { Modal } from "@/components/Modal";
|
||||
import { Divider, Text } from "@tremor/react";
|
||||
|
||||
interface Props {
|
||||
existingConnector: Connector<GoogleDriveConfig>;
|
||||
@ -15,25 +17,28 @@ interface Props {
|
||||
|
||||
export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
|
||||
return (
|
||||
<div
|
||||
className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
|
||||
onClick={onSubmit}
|
||||
>
|
||||
<div
|
||||
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<div className="flex border-b border-gray-600 pb-2 mb-2">
|
||||
<h3 className="text-lg font-semibold w-full">
|
||||
<Modal onOutsideClick={onSubmit}>
|
||||
<div className="px-8 py-6 bg-background">
|
||||
<h2 className="text-xl font-bold flex">
|
||||
Update Google Drive Connector
|
||||
</h3>
|
||||
<div onClick={onSubmit}>
|
||||
<div
|
||||
onClick={onSubmit}
|
||||
className="ml-auto hover:bg-hover p-1.5 rounded"
|
||||
>
|
||||
<XIcon
|
||||
size={30}
|
||||
className="my-auto flex flex-shrink-0 cursor-pointer hover:text-blue-400"
|
||||
size={20}
|
||||
className="my-auto flex flex-shrink-0 cursor-pointer"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</h2>
|
||||
|
||||
<Text>
|
||||
Modify the selected Google Drive connector by adjusting the values
|
||||
below!
|
||||
</Text>
|
||||
|
||||
<Divider />
|
||||
|
||||
<UpdateConnectorForm<GoogleDriveConfig>
|
||||
nameBuilder={googleDriveConnectorNameBuilder}
|
||||
existingConnector={existingConnector}
|
||||
@ -67,6 +72,6 @@ export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
|
||||
onSubmit={onSubmit}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
@ -15,6 +15,7 @@ import { GOOGLE_DRIVE_AUTH_IS_ADMIN_COOKIE_NAME } from "@/lib/constants";
|
||||
import Cookies from "js-cookie";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { Form, Formik } from "formik";
|
||||
import { Card } from "@tremor/react";
|
||||
|
||||
type GoogleDriveCredentialJsonTypes = "authorized_user" | "service_account";
|
||||
|
||||
@ -246,7 +247,7 @@ export const DriveJsonUploadSection = ({
|
||||
<p className="text-sm mb-2">
|
||||
Follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
target="_blank"
|
||||
href="https://docs.danswer.dev/connectors/google_drive#authorization"
|
||||
>
|
||||
@ -322,7 +323,7 @@ export const DriveOAuthSection = ({
|
||||
the documents you want to index with the service account.
|
||||
</p>
|
||||
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
|
||||
<Card>
|
||||
<Formik
|
||||
initialValues={{
|
||||
google_drive_delegated_user: "",
|
||||
@ -385,7 +386,7 @@ export const DriveOAuthSection = ({
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -13,6 +13,14 @@ import { useSWRConfig } from "swr";
|
||||
import { useState } from "react";
|
||||
import { ConnectorEditPopup } from "./ConnectorEditPopup";
|
||||
import { DeleteColumn } from "@/components/admin/connectors/table/DeleteColumn";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@tremor/react";
|
||||
|
||||
interface EditableColumnProps {
|
||||
connectorIndexingStatus: ConnectorIndexingStatus<
|
||||
@ -44,7 +52,7 @@ const EditableColumn = ({ connectorIndexingStatus }: EditableColumnProps) => {
|
||||
className="cursor-pointer"
|
||||
>
|
||||
<div className="mr-2">
|
||||
<EditIcon size={20} />
|
||||
<EditIcon size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -74,6 +82,99 @@ export const GoogleDriveConnectorsTable = ({
|
||||
(a, b) => a.connector.id - b.connector.id
|
||||
);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Table className="overflow-visible">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Edit</TableHeaderCell>
|
||||
<TableHeaderCell>Folder Paths</TableHeaderCell>
|
||||
<TableHeaderCell>Include Shared</TableHeaderCell>
|
||||
<TableHeaderCell>Follow Shortcuts</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Delete</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{sortedGoogleDriveConnectorIndexingStatuses.map(
|
||||
(connectorIndexingStatus) => {
|
||||
return (
|
||||
<TableRow key={connectorIndexingStatus.cc_pair_id}>
|
||||
<TableCell>
|
||||
<EditableColumn
|
||||
connectorIndexingStatus={connectorIndexingStatus}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{(
|
||||
connectorIndexingStatus.connector
|
||||
.connector_specific_config.folder_paths || []
|
||||
).length > 0 ? (
|
||||
<div key={connectorIndexingStatus.connector.id}>
|
||||
{(
|
||||
connectorIndexingStatus.connector
|
||||
.connector_specific_config.folder_paths || []
|
||||
).map((path) => (
|
||||
<div key={path}>
|
||||
<i> - {path}</i>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<i>All Folders</i>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
{connectorIndexingStatus.connector
|
||||
.connector_specific_config.include_shared ? (
|
||||
<i>Yes</i>
|
||||
) : (
|
||||
<i>No</i>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
{connectorIndexingStatus.connector
|
||||
.connector_specific_config.follow_shortcuts ? (
|
||||
<i>Yes</i>
|
||||
) : (
|
||||
<i>No</i>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<StatusRow
|
||||
connectorIndexingStatus={connectorIndexingStatus}
|
||||
hasCredentialsIssue={
|
||||
connectorIndexingStatus.connector.credential_ids
|
||||
.length === 0
|
||||
}
|
||||
setPopup={setPopup}
|
||||
onUpdate={() => {
|
||||
mutate("/api/manage/admin/connector/indexing-status");
|
||||
}}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DeleteColumn
|
||||
connectorIndexingStatus={connectorIndexingStatus}
|
||||
setPopup={setPopup}
|
||||
onUpdate={() =>
|
||||
mutate("/api/manage/admin/connector/indexing-status")
|
||||
}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
}
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<BasicTable
|
||||
columns={[
|
||||
|
@ -24,6 +24,8 @@ import { GoogleDriveConnectorsTable } from "./GoogleDriveConnectorsTable";
|
||||
import { googleDriveConnectorNameBuilder } from "./utils";
|
||||
import { DriveOAuthSection, DriveJsonUploadSection } from "./Credential";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
|
||||
interface GoogleDriveConnectorManagementProps {
|
||||
googleDrivePublicCredential?: Credential<GoogleDriveCredentialJson>;
|
||||
@ -54,10 +56,10 @@ const GoogleDriveConnectorManagement = ({
|
||||
googleDrivePublicCredential || googleDriveServiceAccountCredential;
|
||||
if (!liveCredential) {
|
||||
return (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please authenticate with Google Drive as described in Step 2! Once done
|
||||
with that, you can then move on to enable this connector.
|
||||
</p>
|
||||
</Text>
|
||||
);
|
||||
}
|
||||
|
||||
@ -151,7 +153,7 @@ const GoogleDriveConnectorManagement = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="text-sm">
|
||||
<Text>
|
||||
<div className="my-3">
|
||||
{googleDriveConnectorIndexingStatuses.length > 0 ? (
|
||||
<>
|
||||
@ -169,7 +171,7 @@ const GoogleDriveConnectorManagement = ({
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Text>
|
||||
{googleDriveConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<div className="text-sm mb-2 font-bold">Existing Connectors:</div>
|
||||
@ -179,13 +181,14 @@ const GoogleDriveConnectorManagement = ({
|
||||
}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{googleDriveConnectorIndexingStatuses.length > 0 && (
|
||||
<h2 className="font-bold mt-3 text-sm">Add New Connector:</h2>
|
||||
)}
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
<Card className="mt-4">
|
||||
<ConnectorForm<GoogleDriveConfig>
|
||||
nameBuilder={googleDriveConnectorNameBuilder}
|
||||
source="google_drive"
|
||||
@ -239,7 +242,7 @@ const GoogleDriveConnectorManagement = ({
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={liveCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -353,18 +356,18 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
<DriveJsonUploadSection
|
||||
setPopup={setPopup}
|
||||
appCredentialData={appCredentialData}
|
||||
serviceAccountCredentialData={serviceAccountKeyData}
|
||||
/>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Authenticate with Danswer
|
||||
</h2>
|
||||
</Title>
|
||||
<DriveOAuthSection
|
||||
setPopup={setPopup}
|
||||
refreshCredentials={refreshCredentials}
|
||||
@ -376,9 +379,9 @@ const Main = () => {
|
||||
serviceAccountKeyData={serviceAccountKeyData}
|
||||
/>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 3: Start Indexing!
|
||||
</h2>
|
||||
</Title>
|
||||
<GoogleDriveConnectorManagement
|
||||
googleDrivePublicCredential={googleDrivePublicCredential}
|
||||
googleDriveServiceAccountCredential={
|
||||
@ -401,10 +404,11 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<GoogleDriveIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Google Drive</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<GoogleDriveIcon size={32} />}
|
||||
title="Google Drive"
|
||||
/>
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
|
@ -17,6 +17,8 @@ import { linkCredential } from "@/lib/credential";
|
||||
import { FileUpload } from "@/components/admin/connectors/FileUpload";
|
||||
import { SingleUseConnectorsTable } from "@/components/admin/connectors/table/SingleUseConnectorsTable";
|
||||
import { Spinner } from "@/components/Spinner";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Button, Card, Text, Title } from "@tremor/react";
|
||||
|
||||
export default function GoogleSites() {
|
||||
const { mutate } = useSWRConfig();
|
||||
@ -50,11 +52,13 @@ export default function GoogleSites() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
||||
<GoogleSitesIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Google Sites</h1>
|
||||
</div>
|
||||
<p className="text-sm mb-2">
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<GoogleSitesIcon size={32} />}
|
||||
title="Google Sites"
|
||||
/>
|
||||
|
||||
<Text className="mb-2">
|
||||
For an in-depth guide on how to setup this connector, check out{" "}
|
||||
<a
|
||||
href="https://docs.danswer.dev/connectors/google_sites"
|
||||
@ -64,10 +68,11 @@ export default function GoogleSites() {
|
||||
the documentation
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
</Text>
|
||||
|
||||
<div className="mt-4">
|
||||
<h2 className="font-bold text-xl mb-2">Upload Files</h2>
|
||||
<Title className="mb-2">Upload Files</Title>
|
||||
<Card>
|
||||
<div className="mx-auto w-full">
|
||||
<Formik
|
||||
initialValues={{
|
||||
@ -164,7 +169,7 @@ export default function GoogleSites() {
|
||||
}}
|
||||
>
|
||||
{({ values, isSubmitting }) => (
|
||||
<Form className="p-3 border border-gray-600 rounded">
|
||||
<Form>
|
||||
<TextFormField
|
||||
name="base_url"
|
||||
label="Base URL:"
|
||||
@ -179,12 +184,11 @@ export default function GoogleSites() {
|
||||
setSelectedFiles={setSelectedFiles}
|
||||
message="Upload a zip file containing the HTML of your Google Site"
|
||||
/>
|
||||
<button
|
||||
className={
|
||||
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||
"focus:shadow-outline w-full mx-auto mt-4"
|
||||
}
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="mt-4 w-64 mx-auto"
|
||||
size="xs"
|
||||
color="green"
|
||||
type="submit"
|
||||
disabled={
|
||||
selectedFiles.length !== 1 ||
|
||||
@ -193,11 +197,13 @@ export default function GoogleSites() {
|
||||
}
|
||||
>
|
||||
Upload!
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
</Formik>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -70,23 +72,23 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
This connector allows you to sync all your Guru Cards into Danswer.
|
||||
</p>
|
||||
</Text>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{guruCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Access Token: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md truncate">
|
||||
<Text className="my-auto">Existing Access Token: </Text>
|
||||
<Text className="ml-1 italic my-auto max-w-md truncate">
|
||||
{guruCredential.credential_json?.guru_user_token}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (guruConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -106,18 +108,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the Guru connector, first follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://help.getguru.com/s/article/how-to-obtain-your-api-credentials"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
to generate a User Token.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<GuruCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -147,21 +149,21 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Start indexing!
|
||||
</h2>
|
||||
</Title>
|
||||
{guruCredential ? (
|
||||
!guruConnectorIndexingStatuses.length ? (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
Click the button below to start indexing! We will pull the latest
|
||||
features, components, and products from Guru every <b>10</b>{" "}
|
||||
minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="flex">
|
||||
<ConnectorForm<GuruConfig>
|
||||
nameBuilder={() => "GuruConnector"}
|
||||
@ -178,10 +180,10 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
Guru connector is setup! We are pulling the latest cards from Guru
|
||||
every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<ConnectorsTable<GuruConfig, GuruCredentialJson>
|
||||
connectorIndexingStatuses={guruConnectorIndexingStatuses}
|
||||
liveCredential={guruCredential}
|
||||
@ -206,10 +208,10 @@ const Main = () => {
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with
|
||||
that, you can then start indexing all your Guru cards.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -222,10 +224,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<GuruIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Guru</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<GuruIcon size={32} />} title="Guru" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -72,23 +74,23 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
This connector allows you to sync all your HubSpot Tickets into Danswer.
|
||||
</p>
|
||||
</Text>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{hubSpotCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Access Token: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md truncate">
|
||||
<Text className="my-auto">Existing Access Token: </Text>
|
||||
<Text className="ml-1 italic my-auto max-w-md truncate">
|
||||
{hubSpotCredential.credential_json?.hubspot_access_token}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (hubSpotConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -108,10 +110,10 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the HubSpot connector, provide the HubSpot Access Token.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<HubSpotCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -136,20 +138,20 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Start indexing!
|
||||
</h2>
|
||||
</Title>
|
||||
{hubSpotCredential ? (
|
||||
!hubSpotConnectorIndexingStatuses.length ? (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
Click the button below to start indexing! We will pull the latest
|
||||
tickets from HubSpot every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="flex">
|
||||
<ConnectorForm<HubSpotConfig>
|
||||
nameBuilder={() => "HubSpotConnector"}
|
||||
@ -170,10 +172,10 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
HubSpot connector is setup! We are pulling the latest tickets from
|
||||
HubSpot every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<ConnectorsTable<HubSpotConfig, HubSpotCredentialJson>
|
||||
connectorIndexingStatuses={hubSpotConnectorIndexingStatuses}
|
||||
liveCredential={hubSpotCredential}
|
||||
@ -198,10 +200,10 @@ const Main = () => {
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with
|
||||
that, you can then start indexing all your HubSpot tickets.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -214,10 +216,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<HubSpotIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">HubSpot</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<HubSpotIcon size={32} />} title="HubSpot" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -6,7 +6,6 @@ import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { CredentialForm } from "@/components/admin/connectors/CredentialForm";
|
||||
import {
|
||||
Credential,
|
||||
JiraConfig,
|
||||
JiraCredentialJson,
|
||||
ConnectorIndexingStatus,
|
||||
@ -19,6 +18,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
|
||||
// Copied from the `extract_jira_project` function
|
||||
const extractJiraProject = (url: string): string | null => {
|
||||
@ -82,9 +83,9 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{jiraCredential ? (
|
||||
<>
|
||||
@ -132,17 +133,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the Jira connector, first follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://docs.danswer.dev/connectors/jira#setting-up"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
to generate an Access Token.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<JiraCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -172,18 +174,18 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TODO: make this periodic */}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which spaces do you want to make searchable?
|
||||
</h2>
|
||||
</Title>
|
||||
{jiraCredential ? (
|
||||
<>
|
||||
{" "}
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
Specify any link to a Jira page below and click "Index" to
|
||||
Index. Based on the provided link, we will index the ENTIRE PROJECT,
|
||||
not just the specified page. For example, entering{" "}
|
||||
@ -192,13 +194,13 @@ const Main = () => {
|
||||
</i>{" "}
|
||||
and clicking the Index button will index the whole <i>DAN</i> Jira
|
||||
project.
|
||||
</p>
|
||||
</Text>
|
||||
{jiraConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We pull the latest pages and comments from each space listed
|
||||
below every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<JiraConfig, JiraCredentialJson>
|
||||
connectorIndexingStatuses={jiraConnectorIndexingStatuses}
|
||||
@ -239,9 +241,10 @@ const Main = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Add a New Project</h2>
|
||||
<ConnectorForm<JiraConfig>
|
||||
nameBuilder={(values) =>
|
||||
@ -271,15 +274,15 @@ const Main = () => {
|
||||
}}
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with
|
||||
that, you can then specify which Jira projects you want to make
|
||||
searchable.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -292,10 +295,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<JiraIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Jira</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<JiraIcon size={32} />} title="Jira" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -18,6 +18,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -70,19 +72,19 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{linearCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing API Key: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md truncate">
|
||||
<Text className="my-auto">Existing API Key: </Text>
|
||||
<Text className="ml-1 italic my-auto max-w-md truncate">
|
||||
{linearCredential.credential_json?.linear_api_key}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (linearConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -102,17 +104,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the Linear connector, first follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
href="https://docs.danswer.dev/connectors/linear"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
to generate an API Key.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<LinearCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -137,21 +140,21 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Start indexing
|
||||
</h2>
|
||||
</Title>
|
||||
{linearCredential ? (
|
||||
<>
|
||||
{linearConnectorIndexingStatuses.length > 0 ? (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We pull the latest <i>issues</i> and <i>comments</i> every{" "}
|
||||
<b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<{}, LinearCredentialJson>
|
||||
connectorIndexingStatuses={linearConnectorIndexingStatuses}
|
||||
@ -176,7 +179,7 @@ const Main = () => {
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Create Connector</h2>
|
||||
<p className="text-sm mb-4">
|
||||
Press connect below to start the connection Linear. We pull the
|
||||
@ -194,15 +197,15 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={linearCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with
|
||||
that, you can then start indexing Linear.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -215,10 +218,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<LinearIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Linear</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<LinearIcon size={32} />} title="Linear" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -69,9 +71,9 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your authorization details
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{notionCredential ? (
|
||||
<>
|
||||
@ -101,7 +103,7 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To get started you'll need to create an internal integration in
|
||||
Notion for Danswer. Follow the instructions in the
|
||||
<a
|
||||
@ -115,8 +117,8 @@ const Main = () => {
|
||||
token and paste it below. Follow the remaining instructions on the
|
||||
Notion docs to allow Danswer to read Notion Databases and Pages
|
||||
using the new integration.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
|
||||
</Text>
|
||||
<Card className="mt-2 mb-4">
|
||||
<CredentialForm<NotionCredentialJson>
|
||||
formBody={
|
||||
<TextFormField
|
||||
@ -140,18 +142,18 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Manage Connectors
|
||||
</h2>
|
||||
</Title>
|
||||
{notionConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
The latest page updates are fetched from Notion every 10 minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<NotionConfig, NotionCredentialJson>
|
||||
connectorIndexingStatuses={notionConnectorIndexingStatuses}
|
||||
@ -183,12 +185,13 @@ const Main = () => {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{notionCredential && (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-1">Create New Connection</h2>
|
||||
<p className="text-sm mb-4">
|
||||
Press connect below to start the connection to Notion.
|
||||
@ -226,17 +229,17 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={notionCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!notionCredential && (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
Please provide your integration details in Step 1 first! Once done
|
||||
with that, you'll be able to start the connection then see
|
||||
indexing status.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -249,10 +252,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<NotionIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Notion</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<NotionIcon size={32} />} title="Notion" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -72,29 +74,29 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
This connector allows you to sync all your <i>Features</i>,{" "}
|
||||
<i>Components</i>, <i>Products</i>, and <i>Objectives</i> from
|
||||
Productboard into Danswer. At this time, the Productboard APIs does not
|
||||
support pulling in <i>Releases</i> or <i>Notes</i>.
|
||||
</p>
|
||||
</Text>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{productboardCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Access Token: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md truncate">
|
||||
<Text className="my-auto">Existing Access Token: </Text>
|
||||
<Text className="ml-1 italic my-auto max-w-md truncate">
|
||||
{
|
||||
productboardCredential.credential_json
|
||||
?.productboard_access_token
|
||||
}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (productboardConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -114,17 +116,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the Productboard connector, first follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://developer.productboard.com/#section/Authentication/Public-API-Access-Token"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
to generate an Access Token.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<ProductboardCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -149,21 +152,21 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Start indexing!
|
||||
</h2>
|
||||
</Title>
|
||||
{productboardCredential ? (
|
||||
!productboardConnectorIndexingStatuses.length ? (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
Click the button below to start indexing! We will pull the latest
|
||||
features, components, and products from Productboard every{" "}
|
||||
<b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="flex">
|
||||
<ConnectorForm<ProductboardConfig>
|
||||
nameBuilder={() => "ProductboardConnector"}
|
||||
@ -180,11 +183,11 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
Productboard connector is setup! We are pulling the latest
|
||||
features, components, and products from Productboard every{" "}
|
||||
<b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<ConnectorsTable<ProductboardConfig, ProductboardCredentialJson>
|
||||
connectorIndexingStatuses={productboardConnectorIndexingStatuses}
|
||||
liveCredential={productboardCredential}
|
||||
@ -211,11 +214,11 @@ const Main = () => {
|
||||
)
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with
|
||||
that, you can then start indexing all your Productboard features,
|
||||
components, and products.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -228,10 +231,12 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<ProductboardIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Productboard</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<ProductboardIcon size={32} />}
|
||||
title="Productboard"
|
||||
/>
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
|
||||
const MainSection = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
@ -71,18 +73,18 @@ const MainSection = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide Request Tracker credentials
|
||||
</h2>
|
||||
</Title>
|
||||
{requestTrackerCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Request Tracker username: </p>
|
||||
<p className="ml-1 italic my-auto">
|
||||
<Text className="my-auto">Existing Request Tracker username: </Text>
|
||||
<Text className="ml-1 italic my-auto">
|
||||
{requestTrackerCredential.credential_json.requesttracker_username}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
await adminDeleteCredential(requestTrackerCredential.id);
|
||||
refreshCredentials();
|
||||
@ -94,20 +96,24 @@ const MainSection = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
To use the Request Tracker connector, provide a Request Tracker
|
||||
username, password, and base url.
|
||||
</p>
|
||||
<p className="text-sm mb-2">
|
||||
</Text>
|
||||
<Text className="mb-2">
|
||||
This connector currently supports{" "}
|
||||
<a href="https://rt-wiki.bestpractical.com/wiki/REST">
|
||||
<a
|
||||
className="text-link"
|
||||
href="https://rt-wiki.bestpractical.com/wiki/REST"
|
||||
target="_blank"
|
||||
>
|
||||
Request Tracker REST API 1.0
|
||||
</a>
|
||||
,{" "}
|
||||
<b>not the latest REST API 2.0 introduced in Request Tracker 5.0</b>
|
||||
.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-2">
|
||||
<CredentialForm<RequestTrackerCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -150,21 +156,25 @@ const MainSection = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Manage Request Tracker Connector
|
||||
</Title>
|
||||
|
||||
{requestTrackerConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We index the most recently updated tickets from each Request Tracker
|
||||
instance listed below regularly.
|
||||
</p>
|
||||
<p className="text-sm mb-2">
|
||||
</Text>
|
||||
<Text className="mb-2">
|
||||
The initial poll at this time retrieves tickets updated in the past
|
||||
hour. All subsequent polls execute every ten minutes. This should be
|
||||
configurable in the future.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<RequestTrackerConfig, RequestTrackerCredentialJson>
|
||||
connectorIndexingStatuses={
|
||||
@ -193,10 +203,7 @@ const MainSection = () => {
|
||||
|
||||
{requestTrackerCredential &&
|
||||
requestTrackerConnectorIndexingStatuses.length === 0 ? (
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<h2 className="font-bold mb-3">
|
||||
Step 2: (Re)initialize connection to Request Tracker installation
|
||||
</h2>
|
||||
<Card className="mt-4">
|
||||
<ConnectorForm<RequestTrackerConfig>
|
||||
nameBuilder={(values) =>
|
||||
`RequestTracker-${requestTrackerCredential.credential_json.requesttracker_base_url}`
|
||||
@ -212,7 +219,7 @@ const MainSection = () => {
|
||||
credentialId={requestTrackerCredential.id}
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
@ -226,10 +233,12 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<RequestTrackerIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Request Tracker</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle
|
||||
icon={<RequestTrackerIcon size={32} />}
|
||||
title="Request Tracker"
|
||||
/>
|
||||
|
||||
<MainSection />
|
||||
</div>
|
||||
);
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -71,19 +73,19 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{slabCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Slab Bot Token: </p>
|
||||
<p className="ml-1 italic my-auto max-w-md truncate">
|
||||
<Text className="my-auto">Existing Slab Bot Token: </Text>
|
||||
<Text className="ml-1 italic my-auto max-w-md truncate">
|
||||
{slabCredential.credential_json?.slab_bot_token}
|
||||
</p>
|
||||
</Text>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (slabConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -103,17 +105,18 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To use the Slab connector, first follow the guide{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://docs.danswer.dev/connectors/slab"
|
||||
target="_blank"
|
||||
>
|
||||
here
|
||||
</a>{" "}
|
||||
to generate a Slab Bot Token.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="p-6 mt-2">
|
||||
<CredentialForm<SlabCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -138,18 +141,18 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: What's the base URL for your Slab team?
|
||||
</h2>
|
||||
</Title>
|
||||
{slabCredential ? (
|
||||
<>
|
||||
{slabConnectorIndexingStatuses.length > 0 ? (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We are pulling the latest documents from{" "}
|
||||
<a
|
||||
href={
|
||||
@ -164,7 +167,7 @@ const Main = () => {
|
||||
}
|
||||
</a>{" "}
|
||||
every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<ConnectorsTable<SlabConfig, SlabCredentialJson>
|
||||
connectorIndexingStatuses={slabConnectorIndexingStatuses}
|
||||
liveCredential={slabCredential}
|
||||
@ -206,14 +209,14 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
Specify the base URL for your Slab team below. This will look
|
||||
something like:{" "}
|
||||
<b>
|
||||
<i>https://danswer.slab.com/</i>
|
||||
</b>
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Add a New Space</h2>
|
||||
<ConnectorForm<SlabConfig>
|
||||
nameBuilder={(values) => `SlabConnector-${values.base_url}`}
|
||||
@ -236,17 +239,17 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={slabCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your access token in Step 1 first! Once done with
|
||||
that, you can then specify the URL for your Slab team and get
|
||||
started with indexing.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -259,10 +262,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<SlabIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Slab</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<SlabIcon size={32} />} title="Slab" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -21,6 +21,8 @@ import {
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Button, Card, Divider, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const MainSection = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
@ -69,25 +71,27 @@ const MainSection = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
{slackCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing Slack Bot Token: </p>
|
||||
<p className="ml-1 italic my-auto">
|
||||
<Text className="my-auto">Existing Slack Bot Token: </Text>
|
||||
<Text className="ml-1 italic my-auto">
|
||||
{slackCredential.credential_json.slack_bot_token}
|
||||
</p>{" "}
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
</Text>
|
||||
<Button
|
||||
size="xs"
|
||||
color="red"
|
||||
className="ml-3 text-inverted"
|
||||
onClick={async () => {
|
||||
await adminDeleteCredential(slackCredential.id);
|
||||
refreshCredentials();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
@ -104,7 +108,7 @@ const MainSection = () => {
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
<Card>
|
||||
<CredentialForm<SlackCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -129,20 +133,20 @@ const MainSection = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which channels do you want to make searchable?
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{slackConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We pull the latest messages from each workspace listed below every{" "}
|
||||
<b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<SlackConfig, SlackCredentialJson>
|
||||
connectorIndexingStatuses={slackConnectorIndexingStatuses}
|
||||
@ -181,11 +185,12 @@ const MainSection = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
{slackCredential ? (
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card>
|
||||
<h2 className="font-bold mb-3">Connect to a New Workspace</h2>
|
||||
<ConnectorForm<SlackConfig>
|
||||
nameBuilder={(values) =>
|
||||
@ -226,13 +231,13 @@ const MainSection = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={slackCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your slack bot token in Step 1 first! Once done with
|
||||
that, you can then specify which Slack channels you want to make
|
||||
searchable.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -244,10 +249,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<SlackIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Slack</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<SlackIcon size={32} />} title="Slack" />
|
||||
|
||||
<MainSection />
|
||||
</div>
|
||||
);
|
||||
|
@ -18,7 +18,8 @@ import { HealthCheckBanner } from "@/components/health/healthcheck";
|
||||
import { ConnectorIndexingStatus, WebConfig } from "@/lib/types";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { createCredential, linkCredential } from "@/lib/credential";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Title } from "@tremor/react";
|
||||
|
||||
const SCRAPE_TYPE_TO_PRETTY_NAME = {
|
||||
recursive: "Recursive",
|
||||
@ -49,17 +50,16 @@ export default function Web() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
|
||||
<GlobeIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Web</h1>
|
||||
</div>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
|
||||
<AdminPageTitle icon={<GlobeIcon size={32} />} title="Web" />
|
||||
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Specify which websites to index
|
||||
</h2>
|
||||
</Title>
|
||||
<p className="text-sm mb-2">
|
||||
We re-fetch the latest state of the website once a day.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6">
|
||||
<Card>
|
||||
<ConnectorForm<WebConfig>
|
||||
nameBuilder={(values) => `WebConnector-${values.base_url}`}
|
||||
ccPairNameBuilder={(values) => values.base_url}
|
||||
@ -118,11 +118,11 @@ export default function Web() {
|
||||
}}
|
||||
refreshFreq={60 * 60 * 24} // 1 day
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Already Indexed Websites
|
||||
</h2>
|
||||
</Title>
|
||||
{isConnectorIndexingStatusesLoading ? (
|
||||
<LoadingAnimation text="Loading" />
|
||||
) : isConnectorIndexingStatusesError || !connectorIndexingStatuses ? (
|
||||
|
@ -19,6 +19,8 @@ import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
|
||||
const Main = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
@ -69,9 +71,9 @@ const Main = () => {
|
||||
return (
|
||||
<>
|
||||
{popup}
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide your API details
|
||||
</h2>
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Provide your API details
|
||||
</Title>
|
||||
|
||||
{zendeskCredential ? (
|
||||
<>
|
||||
@ -81,7 +83,7 @@ const Main = () => {
|
||||
{zendeskCredential.credential_json?.zendesk_email}
|
||||
</p>
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
if (zendeskConnectorIndexingStatuses.length > 0) {
|
||||
setPopup({
|
||||
@ -101,7 +103,7 @@ const Main = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
To get started you'll need API token details for your Zendesk
|
||||
instance. You can generate this by access the Admin Center of your
|
||||
instance (e.g. https://<subdomain>.zendesk.com/admin/).
|
||||
@ -110,8 +112,8 @@ const Main = () => {
|
||||
with a name. You will also need to provide the e-mail address of a
|
||||
user that the system will impersonate. This is of little consequence
|
||||
as we are only performing read actions.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2 mb-4">
|
||||
</Text>
|
||||
<Card className="mt-4 mb-4">
|
||||
<CredentialForm<ZendeskCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -153,18 +155,18 @@ const Main = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{zendeskConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Zendesk indexing status
|
||||
</h2>
|
||||
<p className="text-sm mb-2">
|
||||
</Title>
|
||||
<Text className="mb-2">
|
||||
The latest article changes are fetched every 10 minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable<ZendeskConfig, ZendeskCredentialJson>
|
||||
connectorIndexingStatuses={zendeskConnectorIndexingStatuses}
|
||||
@ -192,7 +194,7 @@ const Main = () => {
|
||||
|
||||
{zendeskCredential && zendeskConnectorIndexingStatuses.length === 0 && (
|
||||
<>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Create Connection</h2>
|
||||
<p className="text-sm mb-4">
|
||||
Press connect below to start the connection to your Zendesk
|
||||
@ -209,17 +211,17 @@ const Main = () => {
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
credentialId={zendeskCredential.id}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!zendeskCredential && (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
Please provide your API details in Step 1 first! Once done with
|
||||
that, you'll be able to start the connection then see indexing
|
||||
status.
|
||||
</p>
|
||||
</Text>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
@ -232,10 +234,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<ZendeskIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Zendesk</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<ZendeskIcon size={32} />} title="Zendesk" />
|
||||
|
||||
<Main />
|
||||
</div>
|
||||
);
|
||||
|
@ -18,6 +18,8 @@ import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { ConnectorsTable } from "@/components/admin/connectors/table/ConnectorsTable";
|
||||
import { ConnectorForm } from "@/components/admin/connectors/ConnectorForm";
|
||||
import { usePublicCredentials } from "@/lib/hooks";
|
||||
import { Card, Divider, Text, Title } from "@tremor/react";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
|
||||
const MainSection = () => {
|
||||
const { mutate } = useSWRConfig();
|
||||
@ -66,18 +68,18 @@ const MainSection = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 1: Provide Credentials
|
||||
</h2>
|
||||
</Title>
|
||||
{zulipCredential ? (
|
||||
<>
|
||||
<div className="flex mb-1 text-sm">
|
||||
<p className="my-auto">Existing zuliprc file content: </p>
|
||||
<p className="ml-1 italic my-auto">
|
||||
<Text className="my-auto">Existing zuliprc file content: </Text>
|
||||
<Text className="ml-1 italic my-auto">
|
||||
{zulipCredential.credential_json.zuliprc_content}
|
||||
</p>{" "}
|
||||
</Text>{" "}
|
||||
<button
|
||||
className="ml-1 hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
await adminDeleteCredential(zulipCredential.id);
|
||||
refreshCredentials();
|
||||
@ -89,19 +91,20 @@ const MainSection = () => {
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<p className="text-sm mb-4">
|
||||
<Text className="mb-4">
|
||||
To use the Zulip connector, you must first provide content of the
|
||||
zuliprc config file. For more details on setting up the Danswer
|
||||
Zulip connector, see the{" "}
|
||||
<a
|
||||
className="text-blue-500"
|
||||
className="text-link"
|
||||
href="https://docs.danswer.dev/connectors/zulip"
|
||||
target="_blank"
|
||||
>
|
||||
docs
|
||||
</a>
|
||||
.
|
||||
</p>
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-2">
|
||||
</Text>
|
||||
<Card className="mt-4">
|
||||
<CredentialForm<ZulipCredentialJson>
|
||||
formBody={
|
||||
<>
|
||||
@ -126,22 +129,22 @@ const MainSection = () => {
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
|
||||
<h2 className="font-bold mb-2 mt-6 ml-auto mr-auto">
|
||||
<Title className="mb-2 mt-6 ml-auto mr-auto">
|
||||
Step 2: Which workspaces do you want to make searchable?
|
||||
</h2>
|
||||
</Title>
|
||||
|
||||
{zulipCredential ? (
|
||||
<>
|
||||
{zulipConnectorIndexingStatuses.length > 0 && (
|
||||
<>
|
||||
<p className="text-sm mb-2">
|
||||
<Text className="mb-2">
|
||||
We pull the latest messages from each workspace listed below
|
||||
every <b>10</b> minutes.
|
||||
</p>
|
||||
</Text>
|
||||
<div className="mb-2">
|
||||
<ConnectorsTable
|
||||
connectorIndexingStatuses={zulipConnectorIndexingStatuses}
|
||||
@ -176,10 +179,11 @@ const MainSection = () => {
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<Divider />
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className="border-solid border-gray-600 border rounded-md p-6 mt-4">
|
||||
<Card className="mt-4">
|
||||
<h2 className="font-bold mb-3">Connect to a New Realm</h2>
|
||||
<ConnectorForm<ZulipConfig>
|
||||
nameBuilder={(values) => `ZulipConnector-${values.realm_name}`}
|
||||
@ -205,14 +209,14 @@ const MainSection = () => {
|
||||
}}
|
||||
refreshFreq={10 * 60} // 10 minutes
|
||||
/>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
) : (
|
||||
<p className="text-sm">
|
||||
<Text>
|
||||
Please provide your Zulip credentials in Step 1 first! Once done with
|
||||
that, you can then specify which Zulip realms you want to make
|
||||
searchable.
|
||||
</p>
|
||||
</Text>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
@ -224,10 +228,9 @@ export default function Page() {
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="border-solid border-gray-600 border-b mb-4 pb-2 flex">
|
||||
<ZulipIcon size={32} />
|
||||
<h1 className="text-3xl font-bold pl-2">Zulip</h1>
|
||||
</div>
|
||||
|
||||
<AdminPageTitle icon={<ZulipIcon size={32} />} title="Zulip" />
|
||||
|
||||
<MainSection />
|
||||
</div>
|
||||
);
|
||||
|
@ -2,6 +2,7 @@ import { PopupSpec } from "@/components/admin/connectors/Popup";
|
||||
import { useState } from "react";
|
||||
import { updateBoost } from "./lib";
|
||||
import { CheckmarkIcon, EditIcon } from "@/components/icons/icons";
|
||||
import { FiEdit } from "react-icons/fi";
|
||||
|
||||
export const ScoreSection = ({
|
||||
documentId,
|
||||
@ -62,7 +63,7 @@ export const ScoreSection = ({
|
||||
setScore(initialScore.toString());
|
||||
}
|
||||
}}
|
||||
className="border bg-slate-700 text-gray-200 border-gray-300 rounded py-1 px-3 w-16 h-5 my-auto"
|
||||
className="border bg-background-strong border-gray-300 rounded py-1 px-1 w-12 h-4 my-auto"
|
||||
/>
|
||||
<div onClick={onSubmit} className="cursor-pointer my-auto ml-2">
|
||||
<CheckmarkIcon size={16} className="text-green-700" />
|
||||
@ -73,15 +74,15 @@ export const ScoreSection = ({
|
||||
|
||||
return (
|
||||
<div className="h-full flex flex-col">
|
||||
<div className="flex my-auto">
|
||||
<div className={"flex" + (consistentWidth && " w-6")}>
|
||||
<div className="ml-auto my-auto">{initialScore}</div>
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer ml-2 my-auto"
|
||||
className="flex my-auto cursor-pointer hover:bg-hover rounded"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
<EditIcon size={16} />
|
||||
<div className={"flex " + (consistentWidth && " w-6")}>
|
||||
<div className="ml-auto my-auto">{initialScore}</div>
|
||||
</div>
|
||||
<div className="cursor-pointer ml-2 my-auto h-4">
|
||||
<FiEdit size={16} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -30,7 +30,7 @@ const DocumentDisplay = ({
|
||||
return (
|
||||
<div
|
||||
key={document.document_id}
|
||||
className="text-sm border-b border-gray-800 mb-3"
|
||||
className="text-sm border-b border-border mb-3"
|
||||
>
|
||||
<div className="flex relative">
|
||||
<a
|
||||
@ -49,7 +49,7 @@ const DocumentDisplay = ({
|
||||
</a>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-x-2 mt-1 text-xs">
|
||||
<div className="px-1 py-0.5 bg-gray-700 rounded flex">
|
||||
<div className="px-1 py-0.5 bg-hover rounded flex">
|
||||
<p className="mr-1 my-auto">Boost:</p>
|
||||
<ScoreSection
|
||||
documentId={document.document_id}
|
||||
@ -76,11 +76,11 @@ const DocumentDisplay = ({
|
||||
});
|
||||
}
|
||||
}}
|
||||
className="px-1 py-0.5 bg-gray-700 hover:bg-gray-600 rounded flex cursor-pointer select-none"
|
||||
className="px-1 py-0.5 bg-hover hover:bg-hover-light rounded flex cursor-pointer select-none"
|
||||
>
|
||||
<div className="my-auto">
|
||||
{document.hidden ? (
|
||||
<div className="text-red-500">Hidden</div>
|
||||
<div className="text-error">Hidden</div>
|
||||
) : (
|
||||
"Visible"
|
||||
)}
|
||||
@ -95,7 +95,7 @@ const DocumentDisplay = ({
|
||||
<DocumentUpdatedAtBadge updatedAt={document.updated_at} />
|
||||
</div>
|
||||
)}
|
||||
<p className="pl-1 pt-2 pb-3 text-gray-200 break-words">
|
||||
<p className="pl-1 pt-2 pb-3 break-words">
|
||||
{buildDocumentSummaryDisplay(document.match_highlights, document.blurb)}
|
||||
</p>
|
||||
</div>
|
||||
@ -159,11 +159,11 @@ export function Explorer({
|
||||
<div>
|
||||
{popup}
|
||||
<div className="justify-center py-2">
|
||||
<div className="flex items-center w-full border-2 border-gray-600 rounded px-4 py-2 focus-within:border-blue-500">
|
||||
<MagnifyingGlass className="text-gray-400" />
|
||||
<div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent">
|
||||
<MagnifyingGlass />
|
||||
<textarea
|
||||
autoFocus
|
||||
className="flex-grow ml-2 h-6 bg-transparent outline-none placeholder-gray-400 overflow-hidden whitespace-normal resize-none"
|
||||
className="flex-grow ml-2 h-6 bg-transparent outline-none placeholder-subtle overflow-hidden whitespace-normal resize-none"
|
||||
role="textarea"
|
||||
aria-multiline
|
||||
placeholder="Find documents based on title / content..."
|
||||
@ -180,7 +180,7 @@ export function Explorer({
|
||||
suppressContentEditableWarning={true}
|
||||
/>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<div className="mt-4 border-b border-border">
|
||||
<HorizontalFilters
|
||||
{...filterManager}
|
||||
availableDocumentSets={documentSets}
|
||||
@ -203,7 +203,7 @@ export function Explorer({
|
||||
</div>
|
||||
)}
|
||||
{!query && (
|
||||
<div className="flex text-gray-400 mt-3">
|
||||
<div className="flex text-emphasis mt-3">
|
||||
Search for a document above to modify it's boost or hide it from
|
||||
searches.
|
||||
</div>
|
||||
|
@ -1,6 +1,14 @@
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { useState } from "react";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
} from "@tremor/react";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { DocumentBoostStatus } from "@/lib/types";
|
||||
import { updateHiddenStatus } from "../lib";
|
||||
@ -30,7 +38,7 @@ const IsVisibleSection = ({
|
||||
);
|
||||
onUpdate(response);
|
||||
}}
|
||||
className="flex text-red-700 cursor-pointer hover:bg-gray-700 py-1 px-2 w-fit rounded-full"
|
||||
className="flex text-error cursor-pointer hover:bg-hover py-1 px-2 w-fit rounded-full"
|
||||
>
|
||||
<div className="select-none">Hidden</div>
|
||||
<div className="ml-1 my-auto">
|
||||
@ -46,9 +54,9 @@ const IsVisibleSection = ({
|
||||
);
|
||||
onUpdate(response);
|
||||
}}
|
||||
className="flex text-gray-400 cursor-pointer hover:bg-gray-700 py-1 px-2 w-fit rounded-full"
|
||||
className="flex cursor-pointer hover:bg-hover py-1 px-2 w-fit rounded-full"
|
||||
>
|
||||
<div className="text-gray-400 my-auto select-none">Visible</div>
|
||||
<div className="my-auto select-none">Visible</div>
|
||||
<div className="ml-1 my-auto">
|
||||
<CustomCheckbox checked={true} />
|
||||
</div>
|
||||
@ -56,7 +64,7 @@ const IsVisibleSection = ({
|
||||
)
|
||||
}
|
||||
popupContent={
|
||||
<div className="text-xs text-gray-300">
|
||||
<div className="text-xs">
|
||||
{document.hidden ? (
|
||||
<div className="flex">
|
||||
<FiEye className="my-auto mr-1" /> Unhide
|
||||
@ -86,28 +94,21 @@ export const DocumentFeedbackTable = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
{popup}
|
||||
<BasicTable
|
||||
columns={[
|
||||
{
|
||||
header: "Document Name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
header: "Is Searchable?",
|
||||
key: "visible",
|
||||
},
|
||||
{
|
||||
header: "Score",
|
||||
key: "score",
|
||||
alignment: "right",
|
||||
},
|
||||
]}
|
||||
data={documents
|
||||
<Table className="overflow-visible">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Document Name</TableHeaderCell>
|
||||
<TableHeaderCell>Is Searchable?</TableHeaderCell>
|
||||
<TableHeaderCell>Score</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{documents
|
||||
.slice((page - 1) * numToDisplay, page * numToDisplay)
|
||||
.map((document) => {
|
||||
return {
|
||||
name: (
|
||||
return (
|
||||
<TableRow key={document.document_id}>
|
||||
<TableCell className="whitespace-normal break-all">
|
||||
<a
|
||||
className="text-blue-600"
|
||||
href={document.link}
|
||||
@ -116,8 +117,8 @@ export const DocumentFeedbackTable = ({
|
||||
>
|
||||
{document.semantic_id}
|
||||
</a>
|
||||
),
|
||||
visible: (
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<IsVisibleSection
|
||||
document={document}
|
||||
onUpdate={async (response) => {
|
||||
@ -133,10 +134,13 @@ export const DocumentFeedbackTable = ({
|
||||
}
|
||||
}}
|
||||
/>
|
||||
),
|
||||
score: (
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="ml-auto flex w-16">
|
||||
<div key={document.document_id} className="h-10 ml-auto mr-8">
|
||||
<div
|
||||
key={document.document_id}
|
||||
className="h-10 ml-auto mr-8"
|
||||
>
|
||||
<ScoreSection
|
||||
documentId={document.document_id}
|
||||
initialScore={document.boost}
|
||||
@ -145,10 +149,13 @@ export const DocumentFeedbackTable = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
};
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
|
@ -6,6 +6,7 @@ import { useMostReactedToDocuments } from "@/lib/hooks";
|
||||
import { DocumentFeedbackTable } from "./DocumentFeedbackTable";
|
||||
import { numPages, numToDisplay } from "./constants";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Title } from "@tremor/react";
|
||||
|
||||
const Main = () => {
|
||||
const {
|
||||
@ -47,10 +48,10 @@ const Main = () => {
|
||||
|
||||
return (
|
||||
<div className="mb-8">
|
||||
<h2 className="font-bold text-xl mb-2">Most Liked Documents</h2>
|
||||
<Title className="mb-2">Most Liked Documents</Title>
|
||||
<DocumentFeedbackTable documents={mostLikedDocuments} refresh={refresh} />
|
||||
|
||||
<h2 className="font-bold text-xl mb-2 mt-4">Most Disliked Documents</h2>
|
||||
<Title className="mb-2 mt-6">Most Disliked Documents</Title>
|
||||
<DocumentFeedbackTable
|
||||
documents={mostDislikedDocuments}
|
||||
refresh={refresh}
|
||||
@ -61,7 +62,7 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto">
|
||||
<AdminPageTitle
|
||||
icon={<ThumbsUpIcon size={32} />}
|
||||
title="Document Feedback"
|
||||
|
@ -5,6 +5,7 @@ import { createDocumentSet, updateDocumentSet } from "./lib";
|
||||
import { ConnectorIndexingStatus, DocumentSet } from "@/lib/types";
|
||||
import { TextFormField } from "@/components/admin/connectors/Field";
|
||||
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { Button } from "@tremor/react";
|
||||
|
||||
interface SetCreationPopupProps {
|
||||
ccPairs: ConnectorIndexingStatus<any, any>[];
|
||||
@ -28,7 +29,7 @@ export const DocumentSetCreationForm = ({
|
||||
onClick={onClose}
|
||||
>
|
||||
<div
|
||||
className="bg-gray-800 p-6 rounded border border-gray-700 shadow-lg relative w-1/2 text-sm"
|
||||
className="bg-background p-6 rounded border border-border shadow-lg relative w-1/2 text-sm"
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
<Formik
|
||||
@ -87,7 +88,7 @@ export const DocumentSetCreationForm = ({
|
||||
>
|
||||
{({ isSubmitting, values }) => (
|
||||
<Form>
|
||||
<h2 className="text-lg font-bold mb-3">
|
||||
<h2 className="text-lg text-emphasis font-bold mb-3">
|
||||
{isUpdate
|
||||
? "Update a Document Set"
|
||||
: "Create a new Document Set"}
|
||||
@ -105,7 +106,9 @@ export const DocumentSetCreationForm = ({
|
||||
placeholder="Describe what the document set represents"
|
||||
autoCompleteDisabled={true}
|
||||
/>
|
||||
<h2 className="mb-1">Pick your connectors:</h2>
|
||||
<h2 className="mb-1 font-medium text-base">
|
||||
Pick your connectors:
|
||||
</h2>
|
||||
<p className="mb-3 text-xs">
|
||||
All documents indexed by the selected connectors will be a
|
||||
part of this document set.
|
||||
@ -126,13 +129,13 @@ export const DocumentSetCreationForm = ({
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-gray-700
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " hover:bg-gray-700")
|
||||
? " bg-background-strong"
|
||||
: " hover:bg-hover")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
@ -158,17 +161,13 @@ export const DocumentSetCreationForm = ({
|
||||
)}
|
||||
/>
|
||||
<div className="flex">
|
||||
<button
|
||||
<Button
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
className={
|
||||
"bg-slate-500 hover:bg-slate-700 text-white " +
|
||||
"font-bold py-2 px-4 rounded focus:outline-none " +
|
||||
"focus:shadow-outline w-full max-w-sm mx-auto"
|
||||
}
|
||||
className="w-64 mx-auto"
|
||||
>
|
||||
{isUpdate ? "Update!" : "Create!"}
|
||||
</button>
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
)}
|
||||
|
@ -1,6 +1,5 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import { LoadingAnimation, ThreeDotsLoader } from "@/components/Loading";
|
||||
import { PageSelector } from "@/components/PageSelector";
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
@ -10,6 +9,17 @@ import {
|
||||
InfoIcon,
|
||||
TrashIcon,
|
||||
} from "@/components/icons/icons";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Title,
|
||||
Divider,
|
||||
Badge,
|
||||
} from "@tremor/react";
|
||||
import { useConnectorCredentialIndexingStatus } from "@/lib/hooks";
|
||||
import { ConnectorIndexingStatus, DocumentSet } from "@/lib/types";
|
||||
import { useState } from "react";
|
||||
@ -19,7 +29,14 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
|
||||
import { deleteDocumentSet } from "./lib";
|
||||
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { Text } from "@tremor/react";
|
||||
import { Button, Text } from "@tremor/react";
|
||||
import {
|
||||
FiAlertTriangle,
|
||||
FiCheckCircle,
|
||||
FiClock,
|
||||
FiEdit,
|
||||
} from "react-icons/fi";
|
||||
import { DeleteButton } from "@/components/DeleteButton";
|
||||
|
||||
const numToDisplay = 50;
|
||||
|
||||
@ -50,15 +67,15 @@ const EditRow = ({
|
||||
/>
|
||||
)}
|
||||
{isSyncingTooltipOpen && (
|
||||
<div className="flex flex-nowrap absolute w-64 top-0 left-0 mt-8 bg-gray-700 px-3 py-2 rounded shadow-lg">
|
||||
<InfoIcon className="mt-1 flex flex-shrink-0 mr-2 text-gray-300" />{" "}
|
||||
Cannot update while syncing! Wait for the sync to finish, then try
|
||||
again.
|
||||
<div className="flex flex-nowrap absolute w-64 top-0 left-0 mt-8 border border-border bg-background px-3 py-2 rounded shadow-lg">
|
||||
<InfoIcon className="mt-1 flex flex-shrink-0 mr-2" /> Cannot update
|
||||
while syncing! Wait for the sync to finish, then try again.
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className={
|
||||
"my-auto" + (documentSet.is_up_to_date ? " cursor-pointer" : "")
|
||||
"text-emphasis font-medium my-auto p-1 hover:bg-hover-light flex" +
|
||||
(documentSet.is_up_to_date ? " cursor-pointer" : "")
|
||||
}
|
||||
onClick={() => {
|
||||
if (documentSet.is_up_to_date) {
|
||||
@ -76,7 +93,8 @@ const EditRow = ({
|
||||
}
|
||||
}}
|
||||
>
|
||||
<EditIcon />
|
||||
<FiEdit className="text-emphasis mr-1 my-auto" />
|
||||
{documentSet.name}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -110,49 +128,41 @@ const DocumentSetTable = ({
|
||||
|
||||
return (
|
||||
<div>
|
||||
<BasicTable
|
||||
columns={[
|
||||
{
|
||||
header: "Name",
|
||||
key: "name",
|
||||
},
|
||||
{
|
||||
header: "Connectors",
|
||||
key: "ccPairs",
|
||||
},
|
||||
{
|
||||
header: "Status",
|
||||
key: "status",
|
||||
},
|
||||
{
|
||||
header: "Delete",
|
||||
key: "delete",
|
||||
width: "50px",
|
||||
},
|
||||
]}
|
||||
data={documentSets
|
||||
<Title>Existing Document Sets</Title>
|
||||
<Table className="overflow-visible mt-2">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Connectors</TableHeaderCell>
|
||||
<TableHeaderCell>Status</TableHeaderCell>
|
||||
<TableHeaderCell>Delete</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{documentSets
|
||||
.slice((page - 1) * numToDisplay, page * numToDisplay)
|
||||
.map((documentSet) => {
|
||||
return {
|
||||
name: (
|
||||
<div className="flex gap-x-2">
|
||||
return (
|
||||
<TableRow key={documentSet.id}>
|
||||
<TableCell className="whitespace-normal break-all">
|
||||
<div className="flex gap-x-1 text-emphasis">
|
||||
<EditRow
|
||||
documentSet={documentSet}
|
||||
ccPairs={ccPairs}
|
||||
setPopup={setPopup}
|
||||
refreshDocumentSets={refresh}
|
||||
/>{" "}
|
||||
<b className="my-auto">{documentSet.name}</b>
|
||||
/>
|
||||
</div>
|
||||
),
|
||||
ccPairs: (
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div>
|
||||
{documentSet.cc_pair_descriptors.map(
|
||||
(ccPairDescriptor, ind) => {
|
||||
return (
|
||||
<div
|
||||
className={
|
||||
ind !== documentSet.cc_pair_descriptors.length - 1
|
||||
ind !==
|
||||
documentSet.cc_pair_descriptors.length - 1
|
||||
? "mb-3"
|
||||
: ""
|
||||
}
|
||||
@ -169,23 +179,28 @@ const DocumentSetTable = ({
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
status: documentSet.is_up_to_date ? (
|
||||
<div className="text-emerald-600">Up to date!</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{documentSet.is_up_to_date ? (
|
||||
<Badge size="md" color="green" icon={FiCheckCircle}>
|
||||
Up to Date
|
||||
</Badge>
|
||||
) : documentSet.cc_pair_descriptors.length > 0 ? (
|
||||
<div className="text-gray-300 w-10">
|
||||
<LoadingAnimation text="Syncing" />
|
||||
</div>
|
||||
<Badge size="md" color="amber" icon={FiClock}>
|
||||
Syncing
|
||||
</Badge>
|
||||
) : (
|
||||
<div className="text-red-500 w-10">
|
||||
<LoadingAnimation text="Deleting" />
|
||||
</div>
|
||||
),
|
||||
delete: (
|
||||
<div
|
||||
className="cursor-pointer"
|
||||
<Badge size="md" color="red" icon={FiAlertTriangle}>
|
||||
Deleting
|
||||
</Badge>
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<DeleteButton
|
||||
onClick={async () => {
|
||||
const response = await deleteDocumentSet(documentSet.id);
|
||||
const response = await deleteDocumentSet(
|
||||
documentSet.id
|
||||
);
|
||||
if (response.ok) {
|
||||
setPopup({
|
||||
message: `Document set "${documentSet.name}" scheduled for deletion`,
|
||||
@ -200,13 +215,14 @@ const DocumentSetTable = ({
|
||||
}
|
||||
refresh();
|
||||
}}
|
||||
>
|
||||
<TrashIcon />
|
||||
</div>
|
||||
),
|
||||
};
|
||||
})}
|
||||
/>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
<div className="mt-3 flex">
|
||||
<div className="mx-auto">
|
||||
<PageSelector
|
||||
@ -251,7 +267,7 @@ const Main = () => {
|
||||
return (
|
||||
<div className="mb-8">
|
||||
{popup}
|
||||
<Text className="mb-3 text-gray-300">
|
||||
<Text className="mb-3">
|
||||
<b>Document Sets</b> allow you to group logically connected documents
|
||||
into a single bundle. These can then be used as filter when performing
|
||||
searches in the web UI or attached to slack bots to limit the amount of
|
||||
@ -259,21 +275,29 @@ const Main = () => {
|
||||
or with a certain command.
|
||||
</Text>
|
||||
|
||||
<div className="mb-6"></div>
|
||||
<div className="mb-3"></div>
|
||||
|
||||
<div className="flex mb-3">
|
||||
<Button className="ml-2 my-auto" onClick={() => setIsOpen(true)}>
|
||||
<div className="flex mb-6">
|
||||
<Button
|
||||
size="xs"
|
||||
color="green"
|
||||
className="ml-2 my-auto"
|
||||
onClick={() => setIsOpen(true)}
|
||||
>
|
||||
New Document Set
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{documentSets.length > 0 && (
|
||||
<>
|
||||
<Divider />
|
||||
<DocumentSetTable
|
||||
documentSets={documentSets}
|
||||
ccPairs={ccPairs}
|
||||
refresh={refreshDocumentSets}
|
||||
setPopup={setPopup}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
|
||||
{isOpen && (
|
||||
@ -292,7 +316,7 @@ const Main = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="container mx-auto">
|
||||
<AdminPageTitle icon={<BookmarkIcon size={32} />} title="Document Sets" />
|
||||
|
||||
<Main />
|
||||
|
@ -80,7 +80,7 @@ export function CCPairIndexingStatusTable({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
<Table className="overflow-visible">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
@ -96,12 +96,12 @@ export function CCPairIndexingStatusTable({
|
||||
<TableRow
|
||||
key={ccPairsIndexingStatus.cc_pair_id}
|
||||
className={
|
||||
"hover:bg-gradient-to-r hover:from-gray-800 hover:to-indigo-950 cursor-pointer relative"
|
||||
"hover:bg-hover-light bg-background cursor-pointer relative"
|
||||
}
|
||||
>
|
||||
<TableCell>
|
||||
<div className="flex my-auto">
|
||||
<FiEdit className="mr-4 my-auto text-blue-300" />
|
||||
<FiEdit className="mr-4 my-auto" />
|
||||
<div className="whitespace-normal break-all max-w-3xl">
|
||||
<ConnectorTitle
|
||||
connector={ccPairsIndexingStatus.connector}
|
||||
|
@ -60,13 +60,13 @@ function Main() {
|
||||
|
||||
export default function Status() {
|
||||
return (
|
||||
<div className="mx-auto container dark">
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle
|
||||
icon={<NotebookIcon size={32} />}
|
||||
title="Existing Connectors"
|
||||
farRightElement={
|
||||
<Link href="/admin/add-connector">
|
||||
<Button variant="secondary" size="xs">
|
||||
<Button color="green" size="xs">
|
||||
Add Connector
|
||||
</Button>
|
||||
</Link>
|
||||
|
@ -6,6 +6,7 @@ import { KeyIcon, TrashIcon } from "@/components/icons/icons";
|
||||
import { ApiKeyForm } from "@/components/openai/ApiKeyForm";
|
||||
import { GEN_AI_API_KEY_URL } from "@/components/openai/constants";
|
||||
import { fetcher } from "@/lib/fetcher";
|
||||
import { Text, Title } from "@tremor/react";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
const ExistingKeys = () => {
|
||||
@ -19,7 +20,7 @@ const ExistingKeys = () => {
|
||||
}
|
||||
|
||||
if (error) {
|
||||
return <div className="text-red-600">Error loading existing keys</div>;
|
||||
return <div className="text-error">Error loading existing keys</div>;
|
||||
}
|
||||
|
||||
if (!data?.api_key) {
|
||||
@ -28,11 +29,11 @@ const ExistingKeys = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<h2 className="text-lg font-bold mb-2">Existing Key</h2>
|
||||
<Title className="mb-2">Existing Key</Title>
|
||||
<div className="flex mb-1">
|
||||
<p className="text-sm italic my-auto">sk- ****...**{data?.api_key}</p>
|
||||
<button
|
||||
className="ml-1 my-auto hover:bg-gray-700 rounded-full p-1"
|
||||
className="ml-1 my-auto hover:bg-hover rounded p-1"
|
||||
onClick={async () => {
|
||||
await fetch(GEN_AI_API_KEY_URL, {
|
||||
method: "DELETE",
|
||||
@ -49,16 +50,16 @@ const ExistingKeys = () => {
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle title="OpenAI Keys" icon={<KeyIcon size={32} />} />
|
||||
|
||||
<ExistingKeys />
|
||||
|
||||
<h2 className="text-lg font-bold mb-2">Update Key</h2>
|
||||
<p className="text-sm mb-2">
|
||||
<Title className="mb-2 mt-6">Update Key</Title>
|
||||
<Text className="mb-2">
|
||||
Specify an OpenAI API key and click the "Submit" button.
|
||||
</p>
|
||||
<div className="border rounded-md border-gray-700 p-3">
|
||||
</Text>
|
||||
<div className="border rounded-md border-border p-3">
|
||||
<ApiKeyForm
|
||||
handleResponse={(response) => {
|
||||
if (response.ok) {
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
import { Button, Divider, Text } from "@tremor/react";
|
||||
import { ArrayHelpers, ErrorMessage, FieldArray, Form, Formik } from "formik";
|
||||
import { ArrayHelpers, FieldArray, Form, Formik } from "formik";
|
||||
|
||||
import * as Yup from "yup";
|
||||
import { buildFinalPrompt, createPersona, updatePersona } from "./lib";
|
||||
@ -13,7 +13,6 @@ import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import {
|
||||
BooleanFormField,
|
||||
ManualErrorMessage,
|
||||
SelectorFormField,
|
||||
TextFormField,
|
||||
} from "@/components/admin/connectors/Field";
|
||||
@ -24,12 +23,12 @@ function SectionHeader({ children }: { children: string | JSX.Element }) {
|
||||
|
||||
function Label({ children }: { children: string | JSX.Element }) {
|
||||
return (
|
||||
<div className="block font-medium text-base text-gray-200">{children}</div>
|
||||
<div className="block font-medium text-base text-emphasis">{children}</div>
|
||||
);
|
||||
}
|
||||
|
||||
function SubLabel({ children }: { children: string | JSX.Element }) {
|
||||
return <div className="text-sm text-gray-300 mb-2">{children}</div>;
|
||||
return <div className="text-sm text-subtle mb-2">{children}</div>;
|
||||
}
|
||||
|
||||
export function PersonaEditor({
|
||||
@ -65,35 +64,35 @@ export function PersonaEditor({
|
||||
};
|
||||
|
||||
const isUpdate = existingPersona !== undefined && existingPersona !== null;
|
||||
const existingPrompt = existingPersona?.prompts[0] ?? null;
|
||||
|
||||
useEffect(() => {
|
||||
if (isUpdate) {
|
||||
triggerFinalPromptUpdate(
|
||||
existingPersona.system_prompt,
|
||||
existingPersona.task_prompt,
|
||||
existingPrompt!.system_prompt,
|
||||
existingPrompt!.task_prompt,
|
||||
existingPersona.num_chunks === 0
|
||||
);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
{popup}
|
||||
<Formik
|
||||
enableReinitialize={true}
|
||||
initialValues={{
|
||||
name: existingPersona?.name ?? "",
|
||||
description: existingPersona?.description ?? "",
|
||||
system_prompt: existingPersona?.system_prompt ?? "",
|
||||
task_prompt: existingPersona?.task_prompt ?? "",
|
||||
system_prompt: existingPrompt?.system_prompt ?? "",
|
||||
task_prompt: existingPrompt?.task_prompt ?? "",
|
||||
disable_retrieval: (existingPersona?.num_chunks ?? 5) === 0,
|
||||
document_set_ids:
|
||||
existingPersona?.document_sets?.map(
|
||||
(documentSet) => documentSet.id
|
||||
) ?? ([] as number[]),
|
||||
num_chunks: existingPersona?.num_chunks ?? null,
|
||||
apply_llm_relevance_filter:
|
||||
existingPersona?.apply_llm_relevance_filter ?? false,
|
||||
llm_relevance_filter: existingPersona?.llm_relevance_filter ?? false,
|
||||
llm_model_version_override:
|
||||
existingPersona?.llm_model_version_override ?? null,
|
||||
}}
|
||||
@ -108,7 +107,7 @@ export function PersonaEditor({
|
||||
disable_retrieval: Yup.boolean().required(),
|
||||
document_set_ids: Yup.array().of(Yup.number()),
|
||||
num_chunks: Yup.number().max(20).nullable(),
|
||||
apply_llm_relevance_filter: Yup.boolean().required(),
|
||||
llm_relevance_filter: Yup.boolean().required(),
|
||||
llm_model_version_override: Yup.string().nullable(),
|
||||
})
|
||||
.test(
|
||||
@ -148,29 +147,39 @@ export function PersonaEditor({
|
||||
? 0
|
||||
: values.num_chunks || 5;
|
||||
|
||||
let response;
|
||||
let promptResponse;
|
||||
let personaResponse;
|
||||
if (isUpdate) {
|
||||
response = await updatePersona({
|
||||
[promptResponse, personaResponse] = await updatePersona({
|
||||
id: existingPersona.id,
|
||||
existingPromptId: existingPrompt!.id,
|
||||
...values,
|
||||
num_chunks: numChunks,
|
||||
});
|
||||
} else {
|
||||
response = await createPersona({
|
||||
[promptResponse, personaResponse] = await createPersona({
|
||||
...values,
|
||||
num_chunks: numChunks,
|
||||
});
|
||||
}
|
||||
if (response.ok) {
|
||||
router.push(`/admin/personas?u=${Date.now()}`);
|
||||
return;
|
||||
|
||||
let error = null;
|
||||
if (!promptResponse.ok) {
|
||||
error = await promptResponse.text();
|
||||
}
|
||||
if (personaResponse && !personaResponse.ok) {
|
||||
error = await personaResponse.text();
|
||||
}
|
||||
|
||||
if (error) {
|
||||
setPopup({
|
||||
type: "error",
|
||||
message: `Failed to create Persona - ${await response.text()}`,
|
||||
message: `Failed to create Persona - ${error}`,
|
||||
});
|
||||
formikHelpers.setSubmitting(false);
|
||||
} else {
|
||||
router.push(`/admin/personas?u=${Date.now()}`);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{({ isSubmitting, values, setFieldValue }) => (
|
||||
@ -303,13 +312,13 @@ export function PersonaEditor({
|
||||
py-1
|
||||
rounded-lg
|
||||
border
|
||||
border-gray-700
|
||||
border-border
|
||||
w-fit
|
||||
flex
|
||||
cursor-pointer ` +
|
||||
(isSelected
|
||||
? " bg-gray-600"
|
||||
: " bg-gray-900 hover:bg-gray-700")
|
||||
? " bg-hover"
|
||||
: " bg-background hover:bg-hover-light")
|
||||
}
|
||||
onClick={() => {
|
||||
if (isSelected) {
|
||||
@ -354,6 +363,7 @@ export function PersonaEditor({
|
||||
.
|
||||
</Text>
|
||||
|
||||
<div className="w-96">
|
||||
<SelectorFormField
|
||||
name="llm_model_version_override"
|
||||
options={llmOverrideOptions.map((llmOption) => {
|
||||
@ -364,6 +374,7 @@ export function PersonaEditor({
|
||||
})}
|
||||
includeDefault={true}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
@ -401,7 +412,7 @@ export function PersonaEditor({
|
||||
/>
|
||||
|
||||
<BooleanFormField
|
||||
name="apply_llm_relevance_filter"
|
||||
name="llm_relevance_filter"
|
||||
label="Apply LLM Relevance Filter"
|
||||
subtext={
|
||||
"If enabled, the LLM will filter out chunks that are not relevant to the user query."
|
||||
@ -415,7 +426,7 @@ export function PersonaEditor({
|
||||
<div className="flex">
|
||||
<Button
|
||||
className="mx-auto"
|
||||
variant="secondary"
|
||||
color="green"
|
||||
size="md"
|
||||
type="submit"
|
||||
disabled={isSubmitting}
|
||||
|
@ -11,20 +11,22 @@ import {
|
||||
import { Persona } from "./interfaces";
|
||||
import { EditButton } from "@/components/EditButton";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { FiInfo } from "react-icons/fi";
|
||||
|
||||
export function PersonasTable({ personas }: { personas: Persona[] }) {
|
||||
const router = useRouter();
|
||||
|
||||
const sortedPersonas = [...personas];
|
||||
sortedPersonas.sort((a, b) => a.name.localeCompare(b.name));
|
||||
sortedPersonas.sort((a, b) => (a.id > b.id ? 1 : -1));
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
<Table className="overflow-visible">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Name</TableHeaderCell>
|
||||
<TableHeaderCell>Description</TableHeaderCell>
|
||||
<TableHeaderCell>Built-In</TableHeaderCell>
|
||||
<TableHeaderCell></TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
@ -36,10 +38,21 @@ export function PersonasTable({ personas }: { personas: Persona[] }) {
|
||||
<p className="text font-medium">{persona.name}</p>
|
||||
</TableCell>
|
||||
<TableCell>{persona.description}</TableCell>
|
||||
<TableCell>{persona.default_persona ? "Yes" : "No"}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex">
|
||||
<div className="mx-auto">
|
||||
{!persona.default_persona ? (
|
||||
<EditButton
|
||||
onClick={() => router.push(`/admin/personas/${persona.id}`)}
|
||||
onClick={() =>
|
||||
router.push(`/admin/personas/${persona.id}`)
|
||||
}
|
||||
/>
|
||||
) : (
|
||||
"-"
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
|
@ -10,7 +10,6 @@ export function DeletePersonaButton({ personaId }: { personaId: number }) {
|
||||
|
||||
return (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="xs"
|
||||
color="red"
|
||||
onClick={async () => {
|
||||
|
@ -67,7 +67,7 @@ export default async function Page({
|
||||
const defaultLLM = (await defaultLLMResponse.json()) as string;
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
<InstantSSRAutoRefresh />
|
||||
|
||||
<BackButton />
|
||||
|
@ -1,13 +1,27 @@
|
||||
import { DocumentSet } from "@/lib/types";
|
||||
|
||||
export interface Prompt {
|
||||
id: number;
|
||||
name: string;
|
||||
shared: boolean;
|
||||
description: string;
|
||||
system_prompt: string;
|
||||
task_prompt: string;
|
||||
include_citations: boolean;
|
||||
datetime_aware: boolean;
|
||||
default_prompt: boolean;
|
||||
}
|
||||
|
||||
export interface Persona {
|
||||
id: number;
|
||||
name: string;
|
||||
shared: boolean;
|
||||
description: string;
|
||||
system_prompt: string;
|
||||
task_prompt: string;
|
||||
document_sets: DocumentSet[];
|
||||
prompts: Prompt[];
|
||||
num_chunks?: number;
|
||||
apply_llm_relevance_filter?: boolean;
|
||||
llm_relevance_filter?: boolean;
|
||||
llm_filter_extraction?: boolean;
|
||||
llm_model_version_override?: string;
|
||||
default_persona: boolean;
|
||||
}
|
||||
|
@ -1,3 +1,5 @@
|
||||
import { Prompt } from "./interfaces";
|
||||
|
||||
interface PersonaCreationRequest {
|
||||
name: string;
|
||||
description: string;
|
||||
@ -5,41 +7,157 @@ interface PersonaCreationRequest {
|
||||
task_prompt: string;
|
||||
document_set_ids: number[];
|
||||
num_chunks: number | null;
|
||||
apply_llm_relevance_filter: boolean | null;
|
||||
}
|
||||
|
||||
export function createPersona(personaCreationRequest: PersonaCreationRequest) {
|
||||
return fetch("/api/admin/persona", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(personaCreationRequest),
|
||||
});
|
||||
llm_relevance_filter: boolean | null;
|
||||
}
|
||||
|
||||
interface PersonaUpdateRequest {
|
||||
id: number;
|
||||
existingPromptId: number;
|
||||
name: string;
|
||||
description: string;
|
||||
system_prompt: string;
|
||||
task_prompt: string;
|
||||
document_set_ids: number[];
|
||||
num_chunks: number | null;
|
||||
apply_llm_relevance_filter: boolean | null;
|
||||
llm_relevance_filter: boolean | null;
|
||||
}
|
||||
|
||||
export function updatePersona(personaUpdateRequest: PersonaUpdateRequest) {
|
||||
const { id, ...requestBody } = personaUpdateRequest;
|
||||
function promptNameFromPersonaName(personaName: string) {
|
||||
return `default-prompt__${personaName}`;
|
||||
}
|
||||
|
||||
return fetch(`/api/admin/persona/${id}`, {
|
||||
function createPrompt({
|
||||
personaName,
|
||||
systemPrompt,
|
||||
taskPrompt,
|
||||
}: {
|
||||
personaName: string;
|
||||
systemPrompt: string;
|
||||
taskPrompt: string;
|
||||
}) {
|
||||
return fetch("/api/prompt", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
name: promptNameFromPersonaName(personaName),
|
||||
description: `Default prompt for persona ${personaName}`,
|
||||
shared: true,
|
||||
system_prompt: systemPrompt,
|
||||
task_prompt: taskPrompt,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function updatePrompt({
|
||||
promptId,
|
||||
personaName,
|
||||
systemPrompt,
|
||||
taskPrompt,
|
||||
}: {
|
||||
promptId: number;
|
||||
personaName: string;
|
||||
systemPrompt: string;
|
||||
taskPrompt: string;
|
||||
}) {
|
||||
return fetch(`/api/prompt/${promptId}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(requestBody),
|
||||
body: JSON.stringify({
|
||||
name: promptNameFromPersonaName(personaName),
|
||||
description: `Default prompt for persona ${personaName}`,
|
||||
shared: true,
|
||||
system_prompt: systemPrompt,
|
||||
task_prompt: taskPrompt,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function buildPersonaAPIBody(
|
||||
creationRequest: PersonaCreationRequest | PersonaUpdateRequest,
|
||||
promptId: number
|
||||
) {
|
||||
const {
|
||||
name,
|
||||
description,
|
||||
document_set_ids,
|
||||
num_chunks,
|
||||
llm_relevance_filter,
|
||||
} = creationRequest;
|
||||
|
||||
return {
|
||||
name,
|
||||
description,
|
||||
shared: true,
|
||||
num_chunks,
|
||||
llm_relevance_filter,
|
||||
llm_filter_extraction: false,
|
||||
recency_bias: "base_decay",
|
||||
prompt_ids: [promptId],
|
||||
document_set_ids,
|
||||
};
|
||||
}
|
||||
|
||||
export async function createPersona(
|
||||
personaCreationRequest: PersonaCreationRequest
|
||||
): Promise<[Response, Response | null]> {
|
||||
// first create prompt
|
||||
const createPromptResponse = await createPrompt({
|
||||
personaName: personaCreationRequest.name,
|
||||
systemPrompt: personaCreationRequest.system_prompt,
|
||||
taskPrompt: personaCreationRequest.task_prompt,
|
||||
});
|
||||
const promptId = createPromptResponse.ok
|
||||
? (await createPromptResponse.json()).id
|
||||
: null;
|
||||
|
||||
const createPersonaResponse =
|
||||
promptId !== null
|
||||
? await fetch("/api/admin/persona", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(
|
||||
buildPersonaAPIBody(personaCreationRequest, promptId)
|
||||
),
|
||||
})
|
||||
: null;
|
||||
|
||||
return [createPromptResponse, createPersonaResponse];
|
||||
}
|
||||
|
||||
export async function updatePersona(
|
||||
personaUpdateRequest: PersonaUpdateRequest
|
||||
): Promise<[Response, Response | null]> {
|
||||
const { id, existingPromptId, ...requestBody } = personaUpdateRequest;
|
||||
|
||||
// first update prompt
|
||||
const updatePromptResponse = await updatePrompt({
|
||||
promptId: existingPromptId,
|
||||
personaName: personaUpdateRequest.name,
|
||||
systemPrompt: personaUpdateRequest.system_prompt,
|
||||
taskPrompt: personaUpdateRequest.task_prompt,
|
||||
});
|
||||
|
||||
const updatePersonaResponse = updatePromptResponse.ok
|
||||
? await fetch(`/api/admin/persona/${id}`, {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(
|
||||
buildPersonaAPIBody(personaUpdateRequest, existingPromptId)
|
||||
),
|
||||
})
|
||||
: null;
|
||||
|
||||
return [updatePromptResponse, updatePersonaResponse];
|
||||
}
|
||||
|
||||
export function deletePersona(personaId: number) {
|
||||
return fetch(`/api/admin/persona/${personaId}`, {
|
||||
method: "DELETE",
|
||||
|
@ -46,7 +46,7 @@ export default async function Page() {
|
||||
const defaultLLM = (await defaultLLMResponse.json()) as string;
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
<BackButton />
|
||||
|
||||
<AdminPageTitle
|
||||
|
@ -23,7 +23,7 @@ export default async function Page() {
|
||||
const personas = (await personaResponse.json()) as Persona[];
|
||||
|
||||
return (
|
||||
<div className="dark">
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle icon={<RobotIcon size={32} />} title="Personas" />
|
||||
|
||||
<Text className="mb-2">
|
||||
@ -31,7 +31,7 @@ export default async function Page() {
|
||||
for different use cases.
|
||||
</Text>
|
||||
<Text className="mt-2">They allow you to customize:</Text>
|
||||
<div className="text-dark-tremor-content text-sm">
|
||||
<div className="text-sm">
|
||||
<ul className="list-disc mt-2 ml-4">
|
||||
<li>
|
||||
The prompt used by your LLM of choice to respond to the user query
|
||||
@ -40,13 +40,13 @@ export default async function Page() {
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div className="dark">
|
||||
<div>
|
||||
<Divider />
|
||||
|
||||
<Title>Create a Persona</Title>
|
||||
<Link
|
||||
href="/admin/personas/new"
|
||||
className="text-gray-100 flex py-2 px-4 mt-2 border border-gray-800 h-fit cursor-pointer hover:bg-gray-800 text-sm w-36"
|
||||
className="flex py-2 px-4 mt-2 border border-border h-fit cursor-pointer hover:bg-hover text-sm w-36"
|
||||
>
|
||||
<div className="mx-auto flex">
|
||||
<FiPlusSquare className="my-auto mr-2" />
|
||||
|
@ -1,57 +1,66 @@
|
||||
"use client";
|
||||
|
||||
import { Button } from "@/components/Button";
|
||||
import {
|
||||
Table,
|
||||
TableHead,
|
||||
TableRow,
|
||||
TableHeaderCell,
|
||||
TableBody,
|
||||
TableCell,
|
||||
Button,
|
||||
} from "@tremor/react";
|
||||
import { LoadingAnimation } from "@/components/Loading";
|
||||
import { AdminPageTitle } from "@/components/admin/Title";
|
||||
import { BasicTable } from "@/components/admin/connectors/BasicTable";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { UsersIcon } from "@/components/icons/icons";
|
||||
import { fetcher } from "@/lib/fetcher";
|
||||
import { User } from "@/lib/types";
|
||||
import useSWR, { mutate } from "swr";
|
||||
|
||||
const columns = [
|
||||
{
|
||||
header: "Email",
|
||||
key: "email",
|
||||
},
|
||||
{
|
||||
header: "Role",
|
||||
key: "role",
|
||||
},
|
||||
{
|
||||
header: "Promote",
|
||||
key: "promote",
|
||||
},
|
||||
];
|
||||
|
||||
const UsersTable = () => {
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const { data, isLoading, error } = useSWR<User[]>(
|
||||
"/api/manage/users",
|
||||
fetcher
|
||||
);
|
||||
const {
|
||||
data: users,
|
||||
isLoading,
|
||||
error,
|
||||
} = useSWR<User[]>("/api/manage/users", fetcher);
|
||||
|
||||
if (isLoading) {
|
||||
return <LoadingAnimation text="Loading" />;
|
||||
}
|
||||
|
||||
if (error || !data) {
|
||||
return <div className="text-red-600">Error loading users</div>;
|
||||
if (error || !users) {
|
||||
return <div className="text-error">Error loading users</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{popup}
|
||||
<BasicTable
|
||||
columns={columns}
|
||||
data={data.map((user) => {
|
||||
return {
|
||||
email: user.email,
|
||||
role: <i>{user.role === "admin" ? "Admin" : "User"}</i>,
|
||||
promote:
|
||||
user.role !== "admin" ? (
|
||||
|
||||
<Table className="overflow-visible">
|
||||
<TableHead>
|
||||
<TableRow>
|
||||
<TableHeaderCell>Email</TableHeaderCell>
|
||||
<TableHeaderCell>Role</TableHeaderCell>
|
||||
<TableHeaderCell>
|
||||
<div className="flex">
|
||||
<div className="ml-auto">Promote</div>
|
||||
</div>
|
||||
</TableHeaderCell>
|
||||
</TableRow>
|
||||
</TableHead>
|
||||
<TableBody>
|
||||
{users.map((user) => {
|
||||
return (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell>{user.email}</TableCell>
|
||||
<TableCell>
|
||||
<i>{user.role === "admin" ? "Admin" : "User"}</i>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex">
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
onClick={async () => {
|
||||
const res = await fetch(
|
||||
@ -83,19 +92,21 @@ const UsersTable = () => {
|
||||
>
|
||||
Promote to Admin!
|
||||
</Button>
|
||||
) : (
|
||||
""
|
||||
),
|
||||
};
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
/>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Page = () => {
|
||||
return (
|
||||
<div>
|
||||
<div className="mx-auto container">
|
||||
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
|
||||
|
||||
<UsersTable />
|
||||
|
@ -42,7 +42,7 @@ export function SignInButton({
|
||||
|
||||
return (
|
||||
<a
|
||||
className="mt-6 py-3 w-72 bg-blue-900 flex rounded cursor-pointer hover:bg-blue-950"
|
||||
className="mt-6 py-3 w-72 text-gray-100 bg-accent flex rounded cursor-pointer hover:bg-indigo-800"
|
||||
href={authorizeUrl}
|
||||
>
|
||||
{button}
|
||||
|
@ -77,7 +77,7 @@ const Page = async ({
|
||||
<div className="h-16 w-16 mx-auto">
|
||||
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
|
||||
</div>
|
||||
<h2 className="text-center text-xl font-bold mt-4">
|
||||
<h2 className="text-center text-xl text-strong font-bold mt-6">
|
||||
Log In to Danswer
|
||||
</h2>
|
||||
{authUrl && authTypeMetadata && (
|
||||
|
584
web/src/app/chat/Chat.tsx
Normal file
584
web/src/app/chat/Chat.tsx
Normal 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>
|
||||
);
|
||||
};
|
204
web/src/app/chat/ChatPage.tsx
Normal file
204
web/src/app/chat/ChatPage.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
108
web/src/app/chat/ChatPersonaSelector.tsx
Normal file
108
web/src/app/chat/ChatPersonaSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
14
web/src/app/chat/[chatId]/page.tsx
Normal file
14
web/src/app/chat/[chatId]/page.tsx
Normal 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",
|
||||
});
|
||||
}
|
217
web/src/app/chat/documentSidebar/ChatDocumentDisplay.tsx
Normal file
217
web/src/app/chat/documentSidebar/ChatDocumentDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
23
web/src/app/chat/documentSidebar/DocumentSelector.tsx
Normal file
23
web/src/app/chat/documentSidebar/DocumentSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
170
web/src/app/chat/documentSidebar/DocumentSidebar.tsx
Normal file
170
web/src/app/chat/documentSidebar/DocumentSidebar.tsx
Normal 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>
|
||||
);
|
||||
}
|
24
web/src/app/chat/documentSidebar/SelectedDocumentDisplay.tsx
Normal file
24
web/src/app/chat/documentSidebar/SelectedDocumentDisplay.tsx
Normal 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>
|
||||
);
|
||||
}
|
54
web/src/app/chat/interfaces.ts
Normal file
54
web/src/app/chat/interfaces.ts
Normal 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
268
web/src/app/chat/lib.tsx
Normal 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;
|
||||
}
|
235
web/src/app/chat/message/Messages.tsx
Normal file
235
web/src/app/chat/message/Messages.tsx
Normal 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>
|
||||
);
|
||||
};
|
92
web/src/app/chat/message/SearchSummary.tsx
Normal file
92
web/src/app/chat/message/SearchSummary.tsx
Normal 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>
|
||||
);
|
||||
}
|
43
web/src/app/chat/modal/DeleteChatModal.tsx
Normal file
43
web/src/app/chat/modal/DeleteChatModal.tsx
Normal 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>"{chatSessionName.slice(0, 30)}"</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>
|
||||
);
|
||||
};
|
84
web/src/app/chat/modal/FeedbackModal.tsx
Normal file
84
web/src/app/chat/modal/FeedbackModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
35
web/src/app/chat/modal/ModalWrapper.tsx
Normal file
35
web/src/app/chat/modal/ModalWrapper.tsx
Normal 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>
|
||||
);
|
||||
};
|
345
web/src/app/chat/modifiers/ChatFilters.tsx
Normal file
345
web/src/app/chat/modifiers/ChatFilters.tsx
Normal 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>
|
||||
);
|
||||
}
|
71
web/src/app/chat/modifiers/SearchTypeSelector.tsx
Normal file
71
web/src/app/chat/modifiers/SearchTypeSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
25
web/src/app/chat/modifiers/SelectedDocuments.tsx
Normal file
25
web/src/app/chat/modifiers/SelectedDocuments.tsx
Normal 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
12
web/src/app/chat/page.tsx
Normal 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",
|
||||
});
|
||||
}
|
196
web/src/app/chat/sessionSidebar/ChatSidebar.tsx
Normal file
196
web/src/app/chat/sessionSidebar/ChatSidebar.tsx
Normal 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>
|
||||
);
|
||||
};
|
119
web/src/app/chat/sessionSidebar/SessionDisplay.tsx
Normal file
119
web/src/app/chat/sessionSidebar/SessionDisplay.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
1
web/src/app/chat/types.ts
Normal file
1
web/src/app/chat/types.ts
Normal file
@ -0,0 +1 @@
|
||||
export type FeedbackType = "like" | "dislike";
|
@ -1,3 +1,23 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f9fafb; /* Track background color */
|
||||
}
|
||||
|
||||
/* Style the scrollbar handle */
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: #e5e7eb; /* Handle color */
|
||||
border-radius: 10px;
|
||||
}
|
||||
|
||||
/* Handle on hover */
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: #d1d5db; /* Handle color on hover */
|
||||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
width: 8px; /* Vertical scrollbar width */
|
||||
height: 8px; /* Horizontal scrollbar height */
|
||||
}
|
||||
|
@ -21,7 +21,9 @@ export default async function RootLayout({
|
||||
}) {
|
||||
return (
|
||||
<html lang="en">
|
||||
<body className={`${inter.variable} font-sans bg-gray-900 text-gray-100`}>
|
||||
<body
|
||||
className={`${inter.variable} font-sans text-default bg-background`}
|
||||
>
|
||||
{children}
|
||||
</body>
|
||||
</html>
|
||||
|
@ -8,7 +8,7 @@ import { fetchSS } from "@/lib/utilsSS";
|
||||
import { Connector, DocumentSet, User } from "@/lib/types";
|
||||
import { cookies } from "next/headers";
|
||||
import { SearchType } from "@/lib/search/interfaces";
|
||||
import { Persona } from "./admin/personas/interfaces";
|
||||
import { Persona } from "../admin/personas/interfaces";
|
||||
import { WelcomeModal } from "@/components/WelcomeModal";
|
||||
import { unstable_noStore as noStore } from "next/cache";
|
||||
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
|
||||
@ -88,7 +88,7 @@ export default async function Home() {
|
||||
<ApiKeyModal />
|
||||
<InstantSSRAutoRefresh />
|
||||
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />}
|
||||
<div className="px-24 pt-10 flex flex-col items-center min-h-screen bg-gray-900 text-gray-100">
|
||||
<div className="px-24 pt-10 flex flex-col items-center min-h-screen">
|
||||
<div className="w-full">
|
||||
<SearchSection
|
||||
connectors={connectors}
|
@ -13,7 +13,7 @@ export function BackButton() {
|
||||
my-auto
|
||||
flex
|
||||
mb-1
|
||||
hover:bg-gray-800
|
||||
hover:bg-hover-light
|
||||
w-fit
|
||||
p-1
|
||||
pr-2
|
||||
|
89
web/src/components/BasicClickable.tsx
Normal file
89
web/src/components/BasicClickable.tsx
Normal 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>
|
||||
);
|
||||
}
|
29
web/src/components/Bubble.tsx
Normal file
29
web/src/components/Bubble.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -16,13 +16,13 @@ export const CustomCheckbox = ({
|
||||
/>
|
||||
<span className="relative">
|
||||
<span
|
||||
className={`block w-3 h-3 border border-gray-600 rounded ${
|
||||
checked ? "bg-green-700" : "bg-gray-800"
|
||||
className={`block w-3 h-3 border border-border-strong rounded ${
|
||||
checked ? "bg-green-700" : "bg-background"
|
||||
} transition duration-300`}
|
||||
>
|
||||
{checked && (
|
||||
<svg
|
||||
className="absolute top-0 left-0 w-3 h-3 fill-current text-gray-200"
|
||||
className="absolute top-0 left-0 w-3 h-3 fill-current text-inverted"
|
||||
viewBox="0 0 20 20"
|
||||
>
|
||||
<path d="M0 11l2-2 5 5L18 3l2 2L7 18z" />
|
||||
|
28
web/src/components/DeleteButton.tsx
Normal file
28
web/src/components/DeleteButton.tsx
Normal 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>
|
||||
);
|
||||
}
|
@ -314,7 +314,7 @@ export const CustomDropdown = ({
|
||||
{isOpen && (
|
||||
<div
|
||||
onClick={() => setIsOpen(!isOpen)}
|
||||
className="pt-2 absolute bottom w-full z-30 bg-gray-900"
|
||||
className="pt-2 absolute bottom w-full z-30 box-shadow"
|
||||
>
|
||||
{dropdown}
|
||||
</div>
|
||||
@ -323,48 +323,52 @@ export const CustomDropdown = ({
|
||||
);
|
||||
};
|
||||
|
||||
function DefaultDropdownElement({
|
||||
id,
|
||||
export function DefaultDropdownElement({
|
||||
name,
|
||||
icon,
|
||||
description,
|
||||
onSelect,
|
||||
isSelected,
|
||||
isFinal,
|
||||
includeCheckbox = false,
|
||||
}: {
|
||||
id: string | number | null;
|
||||
name: string;
|
||||
icon?: React.FC<{ size?: number; className?: string }>;
|
||||
description?: string;
|
||||
onSelect: (value: string | number | null) => void;
|
||||
isSelected: boolean;
|
||||
isFinal: boolean;
|
||||
onSelect?: () => void;
|
||||
isSelected?: boolean;
|
||||
includeCheckbox?: boolean;
|
||||
}) {
|
||||
console.log(isFinal);
|
||||
return (
|
||||
<div
|
||||
className={`
|
||||
flex
|
||||
px-3
|
||||
mx-1
|
||||
px-2
|
||||
text-sm
|
||||
text-gray-200
|
||||
py-2.5
|
||||
py-1.5
|
||||
my-1
|
||||
select-none
|
||||
cursor-pointer
|
||||
${isFinal ? "" : "border-b border-gray-800"}
|
||||
${
|
||||
isSelected
|
||||
? "bg-dark-tremor-background-muted"
|
||||
: "hover:bg-dark-tremor-background-muted "
|
||||
}
|
||||
bg-background
|
||||
rounded
|
||||
hover:bg-hover-light
|
||||
`}
|
||||
onClick={() => {
|
||||
onSelect(id);
|
||||
}}
|
||||
onClick={onSelect}
|
||||
>
|
||||
<div>
|
||||
{name}
|
||||
{description && (
|
||||
<div className="text-xs text-dark-tremor-content">{description}</div>
|
||||
<div className="flex">
|
||||
{includeCheckbox && (
|
||||
<input
|
||||
type="checkbox"
|
||||
className="mr-2"
|
||||
checked={isSelected}
|
||||
onChange={() => null}
|
||||
/>
|
||||
)}
|
||||
{icon && icon({ size: 16, className: "mr-2 my-auto" })}
|
||||
{name}
|
||||
</div>
|
||||
{description && <div className="text-xs">{description}</div>}
|
||||
</div>
|
||||
{isSelected && (
|
||||
<div className="ml-auto mr-1 my-auto">
|
||||
@ -394,10 +398,11 @@ export function DefaultDropdown({
|
||||
<div
|
||||
className={`
|
||||
border
|
||||
border-gray-800
|
||||
border
|
||||
rounded-lg
|
||||
flex
|
||||
flex-col
|
||||
bg-background
|
||||
max-h-96
|
||||
overflow-y-auto
|
||||
overscroll-contain`}
|
||||
@ -405,13 +410,11 @@ export function DefaultDropdown({
|
||||
{includeDefault && (
|
||||
<DefaultDropdownElement
|
||||
key={-1}
|
||||
id={null}
|
||||
name="Default"
|
||||
onSelect={() => {
|
||||
onSelect(null);
|
||||
}}
|
||||
isSelected={selected === null}
|
||||
isFinal={false}
|
||||
/>
|
||||
)}
|
||||
{options.map((option, ind) => {
|
||||
@ -419,12 +422,10 @@ export function DefaultDropdown({
|
||||
return (
|
||||
<DefaultDropdownElement
|
||||
key={option.value}
|
||||
id={option.value}
|
||||
name={option.name}
|
||||
description={option.description}
|
||||
onSelect={onSelect}
|
||||
onSelect={() => onSelect(option.value)}
|
||||
isSelected={isSelected}
|
||||
isFinal={ind === options.length - 1}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@ -435,16 +436,15 @@ export function DefaultDropdown({
|
||||
className={`
|
||||
flex
|
||||
text-sm
|
||||
text-gray-400
|
||||
bg-background
|
||||
px-3
|
||||
py-1.5
|
||||
rounded-lg
|
||||
border
|
||||
border-gray-800
|
||||
cursor-pointer
|
||||
hover:bg-dark-tremor-background-muted`}
|
||||
border-border
|
||||
cursor-pointer`}
|
||||
>
|
||||
<p className="text-gray-200 line-clamp-1">
|
||||
<p className="line-clamp-1">
|
||||
{selectedOption?.name ||
|
||||
(includeDefault ? "Default" : "Select an option...")}
|
||||
</p>
|
||||
@ -453,3 +453,45 @@ export function DefaultDropdown({
|
||||
</CustomDropdown>
|
||||
);
|
||||
}
|
||||
|
||||
export function ControlledPopup({
|
||||
children,
|
||||
popupContent,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
}: {
|
||||
children: JSX.Element | string;
|
||||
popupContent: JSX.Element | string;
|
||||
isOpen: boolean;
|
||||
setIsOpen: (value: boolean) => void;
|
||||
}) {
|
||||
const filtersRef = useRef<HTMLDivElement>(null);
|
||||
// hides logout popup on any click outside
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (
|
||||
filtersRef.current &&
|
||||
!filtersRef.current.contains(event.target as Node)
|
||||
) {
|
||||
setIsOpen(false);
|
||||
}
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener("mousedown", handleClickOutside);
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("mousedown", handleClickOutside);
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div ref={filtersRef} className="relative">
|
||||
{children}
|
||||
{isOpen && (
|
||||
<div className="absolute top-0 translate-y-[-105%] bg-background border border-border z-30 rounded text-emphasis">
|
||||
{popupContent}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -1,8 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
import { FiChevronLeft, FiEdit } from "react-icons/fi";
|
||||
import { FiEdit } from "react-icons/fi";
|
||||
|
||||
export function EditButton({ onClick }: { onClick: () => void }) {
|
||||
return (
|
||||
@ -11,12 +9,12 @@ export function EditButton({ onClick }: { onClick: () => void }) {
|
||||
my-auto
|
||||
flex
|
||||
mb-1
|
||||
hover:bg-gray-800
|
||||
hover:bg-hover
|
||||
w-fit
|
||||
p-2
|
||||
cursor-pointer
|
||||
rounded-lg
|
||||
border-gray-800
|
||||
border-border
|
||||
text-sm`}
|
||||
onClick={onClick}
|
||||
>
|
||||
|
@ -7,6 +7,8 @@ import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/navigation";
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { CustomDropdown, DefaultDropdownElement } from "./Dropdown";
|
||||
import { FiMessageSquare, FiSearch } from "react-icons/fi";
|
||||
|
||||
interface HeaderProps {
|
||||
user: User | null;
|
||||
@ -51,50 +53,78 @@ export const Header: React.FC<HeaderProps> = ({ user }) => {
|
||||
}, [dropdownOpen]);
|
||||
|
||||
return (
|
||||
<header className="bg-gray-800 text-gray-200 py-4">
|
||||
<div className="mx-8 flex">
|
||||
<Link href="/">
|
||||
<header className="border-b border-border bg-background-emphasis">
|
||||
<div className="mx-8 flex h-16">
|
||||
<Link className="py-4" href="/search">
|
||||
<div className="flex">
|
||||
<div className="h-[32px] w-[30px]">
|
||||
<Image src="/logo.png" alt="Logo" width="1419" height="1520" />
|
||||
</div>
|
||||
<h1 className="flex text-2xl font-bold my-auto">Danswer</h1>
|
||||
<h1 className="flex text-2xl text-strong font-bold my-auto">
|
||||
Danswer
|
||||
</h1>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div
|
||||
className="ml-auto flex items-center cursor-pointer relative"
|
||||
onClick={() => setDropdownOpen(!dropdownOpen)}
|
||||
ref={dropdownRef}
|
||||
<Link
|
||||
href="/search"
|
||||
className="ml-8 h-full flex flex-col hover:bg-hover"
|
||||
>
|
||||
<UserCircle size={24} className="mr-1 hover:text-red-500" />
|
||||
{dropdownOpen && (
|
||||
<div className="w-28 flex my-auto">
|
||||
<div className="mx-auto flex text-strong px-2">
|
||||
<FiSearch className="my-auto mr-1" />
|
||||
<h1 className="flex text-base font-medium my-auto">Search</h1>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<Link href="/chat" className="h-full flex flex-col hover:bg-hover">
|
||||
<div className="w-28 flex my-auto">
|
||||
<div className="mx-auto flex text-strong px-2">
|
||||
<FiMessageSquare className="my-auto mr-1" />
|
||||
<h1 className="flex text-base font-medium my-auto">Chat</h1>
|
||||
</div>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
<div className="ml-auto h-full flex flex-col">
|
||||
<div className="my-auto">
|
||||
<CustomDropdown
|
||||
dropdown={
|
||||
<div
|
||||
className={
|
||||
"absolute top-10 right-0 mt-2 bg-gray-600 rounded-sm " +
|
||||
"w-48 overflow-hidden shadow-xl z-10 text-sm text-gray-300"
|
||||
"absolute right-0 mt-2 bg-background rounded border border-border " +
|
||||
"w-48 overflow-hidden shadow-xl z-10 text-sm"
|
||||
}
|
||||
>
|
||||
{/* Show connector option if (1) auth is disabled or (2) user is an admin */}
|
||||
{(!user || user.role === "admin") && (
|
||||
<Link href="/admin/indexing/status">
|
||||
<div className="flex py-2 px-3 cursor-pointer hover:bg-gray-500 border-b border-gray-500">
|
||||
Admin Panel
|
||||
</div>
|
||||
<DefaultDropdownElement name="Admin Panel" />
|
||||
</Link>
|
||||
)}
|
||||
{user && (
|
||||
<div
|
||||
className="flex py-2 px-3 cursor-pointer hover:bg-gray-500"
|
||||
onClick={handleLogout}
|
||||
<DefaultDropdownElement
|
||||
name="Logout"
|
||||
onSelect={handleLogout}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
>
|
||||
Logout
|
||||
<div className="hover:bg-hover rounded p-1 w-fit">
|
||||
<div className="my-auto bg-user text-sm rounded-lg px-1.5 select-none">
|
||||
{user && user.email ? user.email[0].toUpperCase() : "A"}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CustomDropdown>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
);
|
||||
};
|
||||
|
||||
/*
|
||||
|
||||
*/
|
||||
|
@ -4,7 +4,7 @@ interface HoverPopupProps {
|
||||
mainContent: string | JSX.Element;
|
||||
popupContent: string | JSX.Element;
|
||||
classNameModifications?: string;
|
||||
direction?: "left" | "bottom";
|
||||
direction?: "left" | "bottom" | "top";
|
||||
style?: "basic" | "dark";
|
||||
}
|
||||
|
||||
@ -25,6 +25,9 @@ export const HoverPopup = ({
|
||||
case "bottom":
|
||||
popupDirectionClass = "top-0 left-0 mt-6 pt-2";
|
||||
break;
|
||||
case "top":
|
||||
popupDirectionClass = "top-0 left-0 translate-y-[-100%] pb-2";
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
@ -39,10 +42,7 @@ export const HoverPopup = ({
|
||||
<div className={`absolute ${popupDirectionClass} z-30`}>
|
||||
<div
|
||||
className={
|
||||
`px-3 py-2 rounded ` +
|
||||
(style === "dark"
|
||||
? "bg-dark-tremor-background-muted border border-gray-800"
|
||||
: "bg-gray-800 shadow-lg") +
|
||||
`px-3 py-2 rounded bg-background border border-border` +
|
||||
(classNameModifications || "")
|
||||
}
|
||||
>
|
||||
@ -50,7 +50,7 @@ export const HoverPopup = ({
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="z-20">{mainContent}</div>
|
||||
<div>{mainContent}</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -22,14 +22,14 @@ export function Modal({
|
||||
>
|
||||
<div
|
||||
className={`
|
||||
bg-gray-800 rounded-sm shadow-lg
|
||||
bg-background rounded-sm shadow-lg
|
||||
shadow-lg relative w-1/2 text-sm
|
||||
${className}
|
||||
`}
|
||||
onClick={(event) => event.stopPropagation()}
|
||||
>
|
||||
{title && (
|
||||
<h2 className="text-xl font-bold mb-3 border-b border-gray-700 pt-4 pb-3 bg-gray-700 px-6">
|
||||
<h2 className="text-xl font-bold mb-3 border-b border-border pt-4 pb-3 bg-background-strong px-6">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
|
@ -61,14 +61,13 @@ const PageLink = ({
|
||||
py-1
|
||||
leading-5
|
||||
-ml-px
|
||||
text-gray-300
|
||||
border-gray-600
|
||||
${!unclickable ? "hover:bg-gray-600" : ""}
|
||||
border-border
|
||||
${!unclickable ? "hover:bg-hover" : ""}
|
||||
${!unclickable ? "cursor-pointer" : ""}
|
||||
first:ml-0
|
||||
first:rounded-l-md
|
||||
last:rounded-r-md
|
||||
${active ? "bg-gray-700" : ""}
|
||||
${active ? "bg-background-strong" : ""}
|
||||
`}
|
||||
onClick={() => {
|
||||
if (pageChangeHandler) {
|
||||
|
@ -61,8 +61,7 @@ export function IndexAttemptStatus({
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: remove wrapping `dark` once we have light/dark mode
|
||||
return <div className="dark">{badge}</div>;
|
||||
return <div>{badge}</div>;
|
||||
}
|
||||
|
||||
export function CCPairStatus({
|
||||
@ -104,6 +103,5 @@ export function CCPairStatus({
|
||||
);
|
||||
}
|
||||
|
||||
// TODO: remove wrapping `dark` once we have light/dark mode
|
||||
return <div className="dark">{badge}</div>;
|
||||
return <div>{badge}</div>;
|
||||
}
|
||||
|
@ -7,11 +7,11 @@ import Link from "next/link";
|
||||
export function WelcomeModal() {
|
||||
return (
|
||||
<Modal className="max-w-4xl">
|
||||
<div className="px-6 py-4">
|
||||
<h2 className="text-xl font-bold mb-4 pb-2 border-b border-gray-700 flex">
|
||||
<div className="px-8 py-6">
|
||||
<h2 className="text-xl font-bold mb-4 pb-2 border-b border-border flex">
|
||||
Welcome to Danswer 🎉
|
||||
</h2>
|
||||
<div className="text-gray-100">
|
||||
<div>
|
||||
<p className="mb-4">
|
||||
Danswer is the AI-powered search engine for your organization's
|
||||
internal knowledge. Whenever you need to find any piece of internal
|
||||
@ -26,7 +26,7 @@ export function WelcomeModal() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex mt-3 dark">
|
||||
<div className="flex mt-3">
|
||||
<Link href="/admin/add-connector" className="mx-auto">
|
||||
<Button>Setup your first connector!</Button>
|
||||
</Link>
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { Header } from "@/components/Header";
|
||||
import { Sidebar } from "@/components/admin/connectors/Sidebar";
|
||||
import { AdminSidebar } from "@/components/admin/connectors/AdminSidebar";
|
||||
import {
|
||||
NotebookIcon,
|
||||
KeyIcon,
|
||||
@ -30,11 +30,13 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="h-screen overflow-y-hidden">
|
||||
<div className="absolute top-0 z-50 w-full">
|
||||
<Header user={user} />
|
||||
<div className="bg-gray-900 pt-8 pb-8 flex">
|
||||
<div className="w-72">
|
||||
<Sidebar
|
||||
</div>
|
||||
<div className="flex h-full pt-16">
|
||||
<div className="w-80 pt-12 pb-8 h-full border-r border-border">
|
||||
<AdminSidebar
|
||||
collections={[
|
||||
{
|
||||
name: "Connectors",
|
||||
@ -145,7 +147,9 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="px-12 bg-gray-900 text-gray-100 w-full">{children}</div>
|
||||
<div className="px-12 pt-8 pb-8 h-full overflow-y-auto w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -13,12 +13,12 @@ export function AdminPageTitle({
|
||||
includeDivider?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<div className="dark">
|
||||
<div>
|
||||
<div className="mb-4">
|
||||
<HealthCheckBanner />
|
||||
</div>
|
||||
<div className="flex">
|
||||
<h1 className="text-3xl font-bold flex gap-x-2">
|
||||
<h1 className="text-3xl text-strong font-bold flex gap-x-2">
|
||||
{icon} {title}
|
||||
</h1>
|
||||
{farRightElement && <div className="ml-auto">{farRightElement}</div>}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user