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