FE improvements to make initial setup more intuitive

This commit is contained in:
Weves
2023-12-02 15:12:34 -08:00
committed by Chris Weaver
parent 02095e9281
commit ac35719038
27 changed files with 566 additions and 613 deletions

View File

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

View File

@@ -13,6 +13,7 @@ import { SlackBotCreationForm } from "./SlackBotConfigCreationForm";
import { deleteSlackBotConfig } from "./lib";
import { SlackBotTokensForm } from "./SlackBotTokensForm";
import { useDocumentSets } from "../documents/sets/hooks";
import { AdminPageTitle } from "@/components/admin/Title";
const numToDisplay = 50;
@@ -315,10 +316,10 @@ const Main = () => {
const Page = () => {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<CPUIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Slack Bot Configuration</h1>
</div>
<AdminPageTitle
icon={<CPUIcon size={32} />}
title="Slack Bot Configuration"
/>
<Main />
</div>

View File

@@ -4,7 +4,6 @@ import { adminSearch } from "./lib";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { useState, useEffect } from "react";
import { DanswerDocument } from "@/lib/search/interfaces";
import { getSourceIcon } from "@/components/source";
import { buildDocumentSummaryDisplay } from "@/components/search/DocumentDisplay";
import { CustomCheckbox } from "@/components/CustomCheckbox";
import { updateHiddenStatus } from "../lib";
@@ -17,6 +16,7 @@ import { useFilters } from "@/lib/hooks";
import { buildFilters } from "@/lib/search/utils";
import { DocumentUpdatedAtBadge } from "@/components/search/DocumentUpdatedAtBadge";
import { Connector, DocumentSet } from "@/lib/types";
import { SourceIcon } from "@/components/SourceIcon";
const DocumentDisplay = ({
document,
@@ -42,7 +42,7 @@ const DocumentDisplay = ({
target="_blank"
rel="noopener noreferrer"
>
{getSourceIcon(document.source_type, 22)}
<SourceIcon sourceType={document.source_type} iconSize={22} />
<p className="truncate break-all ml-2 my-auto text-base">
{document.semantic_identifier || document.document_id}
</p>
@@ -111,7 +111,6 @@ export function Explorer({
connectors: Connector<any>[];
documentSets: DocumentSet[];
}) {
console.log(connectors);
const router = useRouter();
const { popup, setPopup } = usePopup();

View File

@@ -5,6 +5,7 @@ import { ThumbsUpIcon } from "@/components/icons/icons";
import { useMostReactedToDocuments } from "@/lib/hooks";
import { DocumentFeedbackTable } from "./DocumentFeedbackTable";
import { numPages, numToDisplay } from "./constants";
import { AdminPageTitle } from "@/components/admin/Title";
const Main = () => {
const {
@@ -61,10 +62,10 @@ const Main = () => {
const Page = () => {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<ThumbsUpIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Document Feedback</h1>
</div>
<AdminPageTitle
icon={<ThumbsUpIcon size={32} />}
title="Document Feedback"
/>
<Main />
</div>

View File

@@ -18,6 +18,8 @@ import { DocumentSetCreationForm } from "./DocumentSetCreationForm";
import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { deleteDocumentSet } from "./lib";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { AdminPageTitle } from "@/components/admin/Title";
import { Text } from "@tremor/react";
const numToDisplay = 50;
@@ -249,15 +251,15 @@ const Main = () => {
return (
<div className="mb-8">
{popup}
<div className="text-sm mb-3">
<Text className="mb-3 text-gray-300">
<b>Document Sets</b> allow you to group logically connected documents
into a single bundle. These can then be used as filter when performing
searches in the web UI or attached to slack bots to limit the amount of
information the bot searches over when answering in a specific channel
or with a certain command.
</div>
</Text>
<div className="mb-2"></div>
<div className="mb-6"></div>
<div className="flex mb-3">
<Button className="ml-2 my-auto" onClick={() => setIsOpen(true)}>
@@ -265,12 +267,14 @@ const Main = () => {
</Button>
</div>
<DocumentSetTable
documentSets={documentSets}
ccPairs={ccPairs}
refresh={refreshDocumentSets}
setPopup={setPopup}
/>
{documentSets.length > 0 && (
<DocumentSetTable
documentSets={documentSets}
ccPairs={ccPairs}
refresh={refreshDocumentSets}
setPopup={setPopup}
/>
)}
{isOpen && (
<DocumentSetCreationForm
@@ -289,10 +293,7 @@ const Main = () => {
const Page = () => {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<BookmarkIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Document Sets</h1>
</div>
<AdminPageTitle icon={<BookmarkIcon size={32} />} title="Document Sets" />
<Main />
</div>

View File

@@ -17,6 +17,7 @@ import { ConnectorTitle } from "@/components/admin/connectors/ConnectorTitle";
import { getDocsProcessedPerMinute } from "@/lib/indexAttempt";
import Link from "next/link";
import { isCurrentlyDeleting } from "@/lib/documentDeletion";
import { FiEdit, FiMaximize, FiMaximize2 } from "react-icons/fi";
const NUM_IN_PAGE = 20;
@@ -98,12 +99,17 @@ export function CCPairIndexingStatusTable({
"hover:bg-gradient-to-r hover:from-gray-800 hover:to-indigo-950 cursor-pointer relative"
}
>
<TableCell className="whitespace-normal break-all">
<ConnectorTitle
connector={ccPairsIndexingStatus.connector}
ccPairId={ccPairsIndexingStatus.cc_pair_id}
ccPairName={ccPairsIndexingStatus.name}
/>
<TableCell>
<div className="flex my-auto">
<FiEdit className="mr-4 my-auto text-blue-300" />
<div className="whitespace-normal break-all max-w-3xl">
<ConnectorTitle
connector={ccPairsIndexingStatus.connector}
ccPairId={ccPairsIndexingStatus.cc_pair_id}
ccPairName={ccPairsIndexingStatus.name}
/>
</div>
</div>
</TableCell>
<TableCell>
<CCPairIndexingStatusDisplay

View File

@@ -5,10 +5,11 @@ import useSWR from "swr";
import { LoadingAnimation } from "@/components/Loading";
import { NotebookIcon } from "@/components/icons/icons";
import { fetcher } from "@/lib/fetcher";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { ConnectorIndexingStatus } from "@/lib/types";
import { CCPairIndexingStatusTable } from "./CCPairIndexingStatusTable";
import { Divider } from "@tremor/react";
import { AdminPageTitle } from "@/components/admin/Title";
import Link from "next/link";
import { Button } from "@tremor/react";
function Main() {
const {
@@ -29,6 +30,18 @@ function Main() {
return <div className="text-red-600">Error loading indexing history.</div>;
}
if (indexAttemptData.length === 0) {
return (
<div className="text-gray-300 text-sm">
It looks like you don&apos;t have any connectors setup yet. Visit the{" "}
<Link className="text-blue-500" href="/admin/add-connector">
Add Connector
</Link>{" "}
page to get started!
</div>
);
}
// sort by source name
indexAttemptData.sort((a, b) => {
if (a.connector.source < b.connector.source) {
@@ -48,13 +61,17 @@ function Main() {
export default function Status() {
return (
<div className="mx-auto container dark">
<div className="mb-4">
<HealthCheckBanner />
</div>
<h1 className="text-3xl font-bold flex gap-x-2 mb-2">
<NotebookIcon size={32} /> Indexing Status
</h1>
<Divider />
<AdminPageTitle
icon={<NotebookIcon size={32} />}
title="Existing Connectors"
farRightElement={
<Link href="/admin/add-connector">
<Button variant="secondary" size="xs">
Add Connector
</Button>
</Link>
}
/>
<Main />
</div>
);

View File

@@ -1,6 +1,7 @@
"use client";
import { LoadingAnimation } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { KeyIcon, TrashIcon } from "@/components/icons/icons";
import { ApiKeyForm } from "@/components/openai/ApiKeyForm";
import { GEN_AI_API_KEY_URL } from "@/components/openai/constants";
@@ -49,10 +50,7 @@ const ExistingKeys = () => {
const Page = () => {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<KeyIcon size={32} />
<h1 className="text-3xl font-bold pl-2">OpenAI Keys</h1>
</div>
<AdminPageTitle title="OpenAI Keys" icon={<KeyIcon size={32} />} />
<ExistingKeys />

View File

@@ -6,6 +6,7 @@ import { fetchSS } from "@/lib/utilsSS";
import { ErrorCallout } from "@/components/ErrorCallout";
import { Persona } from "./interfaces";
import { RobotIcon } from "@/components/icons/icons";
import { AdminPageTitle } from "@/components/admin/Title";
export default async function Page() {
const personaResponse = await fetchSS("/persona");
@@ -23,10 +24,7 @@ export default async function Page() {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<RobotIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Personas</h1>
</div>
<AdminPageTitle icon={<RobotIcon size={32} />} title="Personas" />
<div className="text-gray-300 text-sm mb-2">
Personas are a way to build custom search/question-answering experiences

View File

@@ -2,6 +2,7 @@
import { Button } from "@/components/Button";
import { LoadingAnimation } from "@/components/Loading";
import { AdminPageTitle } from "@/components/admin/Title";
import { BasicTable } from "@/components/admin/connectors/BasicTable";
import { usePopup } from "@/components/admin/connectors/Popup";
import { UsersIcon } from "@/components/icons/icons";
@@ -95,10 +96,7 @@ const UsersTable = () => {
const Page = () => {
return (
<div>
<div className="border-solid border-gray-600 border-b pb-2 mb-4 flex">
<UsersIcon size={32} />
<h1 className="text-3xl font-bold pl-2">Manage Users</h1>
</div>
<AdminPageTitle title="Manage Users" icon={<UsersIcon size={32} />} />
<UsersTable />
</div>

View File

@@ -9,6 +9,7 @@ import { Connector, DocumentSet, User } from "@/lib/types";
import { cookies } from "next/headers";
import { SearchType } from "@/lib/search/interfaces";
import { Persona } from "./admin/personas/interfaces";
import { WelcomeModal } from "@/components/WelcomeModal";
export default async function Home() {
const tasks = [
@@ -78,6 +79,7 @@ export default async function Home() {
<HealthCheckBanner />
</div>
<ApiKeyModal />
{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="w-full">
<SearchSection

View File

@@ -1,32 +1,28 @@
interface ModalProps {
children: JSX.Element | string;
onOutsideClick: () => void;
title?: JSX.Element | string;
onOutsideClick?: () => void;
}
export const Modal: React.FC<ModalProps> = ({
children,
onOutsideClick,
title,
}) => {
export function Modal({ children, title, onOutsideClick }: ModalProps) {
return (
<div>
<div
className={`
fixed inset-0 bg-black bg-opacity-50
fixed inset-0 bg-black bg-opacity-50
flex items-center justify-center z-50
`}
onClick={onOutsideClick}
>
<div
className={`
bg-gray-800 rounded-lg border border-gray-700
bg-gray-800 rounded-sm shadow-lg
shadow-lg relative w-1/2 text-sm
`}
onClick={(event) => event.stopPropagation()}
>
{title && (
<h2 className="text-xl font-bold mb-3 border-b border-gray-600 pt-4 pb-3 bg-gray-700 px-6">
<h2 className="text-xl font-bold mb-3 border-b border-gray-700 pt-4 pb-3 bg-gray-700 px-6">
{title}
</h2>
)}
@@ -35,4 +31,4 @@ export const Modal: React.FC<ModalProps> = ({
</div>
</div>
);
};
}

View File

@@ -0,0 +1,16 @@
"use client";
import { getSourceMetadata } from "@/lib/sources";
import { ValidSources } from "@/lib/types";
export function SourceIcon({
sourceType,
iconSize,
}: {
sourceType: ValidSources;
iconSize: number;
}) {
return getSourceMetadata(sourceType).icon({
size: iconSize,
});
}

View File

@@ -0,0 +1,44 @@
"use client";
import { Button } from "@tremor/react";
import { Modal } from "./Modal";
import Link from "next/link";
import { useState } from "react";
import { FiX } from "react-icons/fi";
export function WelcomeModal() {
const [isClosed, setIsClosed] = useState(false);
if (isClosed) {
return null;
}
return (
<Modal>
<div className="px-6 py-4">
<h2 className="text-xl font-bold mb-4 pb-2 border-b border-gray-700 flex">
Welcome to Danswer 🎉
</h2>
<div className="text-gray-100">
<p className="mb-4">
Danswer is the AI-powered search engine for your organization&apos;s
internal knowledge. Whenever you need to find any piece of internal
information, Danswer is there to help!
</p>
<p>
To get started, the first step is to configure some{" "}
<i>connectors</i>. Connectors are the way that Danswer gets data
from your organization&apos;s various data sources. Once setup,
we&apos;ll automatically sync data from your apps and docs into
Danswer, so you can search all through all of them in one place.
</p>
</div>
<div className="flex mt-3 dark">
<Button className="mx-auto">
<Link href="/admin/add-connector">Setup your first connector!</Link>
</Button>
</div>
</div>
</Modal>
);
}

View File

@@ -2,33 +2,14 @@ import { Header } from "@/components/Header";
import { Sidebar } from "@/components/admin/connectors/Sidebar";
import {
NotebookIcon,
GithubIcon,
GlobeIcon,
GoogleDriveIcon,
SlackIcon,
KeyIcon,
BookstackIcon,
ConfluenceIcon,
GuruIcon,
FileIcon,
JiraIcon,
SlabIcon,
NotionIcon,
ZulipIcon,
ProductboardIcon,
LinearIcon,
UsersIcon,
ThumbsUpIcon,
HubSpotIcon,
BookmarkIcon,
CPUIcon,
Document360Icon,
RequestTrackerIcon,
GoogleSitesIcon,
GongIcon,
ZoomInIcon,
ZendeskIcon,
RobotIcon,
ConnectorIcon,
} from "@/components/icons/icons";
import { getAuthDisabledSS, getCurrentUserSS } from "@/lib/userSS";
import { redirect } from "next/navigation";
@@ -52,310 +33,119 @@ export async function Layout({ children }: { children: React.ReactNode }) {
<div>
<Header user={user} />
<div className="bg-gray-900 pt-8 pb-8 flex">
<Sidebar
title="Connector"
collections={[
{
name: "Indexing",
items: [
{
name: (
<div className="flex">
<NotebookIcon size={18} />
<div className="ml-1">Status</div>
</div>
),
link: "/admin/indexing/status",
},
],
},
{
name: "Connector Settings",
items: [
{
name: (
<div className="flex">
<SlackIcon size={16} />
<div className="ml-1">Slack</div>
</div>
),
link: "/admin/connectors/slack",
},
{
name: (
<div className="flex">
<GithubIcon size={16} />
<div className="ml-1">Github</div>
</div>
),
link: "/admin/connectors/github",
},
{
name: (
<div className="flex">
<GoogleDriveIcon size={16} />
<div className="ml-1">Google Drive</div>
</div>
),
link: "/admin/connectors/google-drive",
},
{
name: (
<div className="flex">
<ConfluenceIcon size={16} />
<div className="ml-1">Confluence</div>
</div>
),
link: "/admin/connectors/confluence",
},
{
name: (
<div className="flex">
<JiraIcon size={16} />
<div className="ml-1">Jira</div>
</div>
),
link: "/admin/connectors/jira",
},
{
name: (
<div className="flex">
<LinearIcon size={16} />
<div className="ml-1">Linear</div>
</div>
),
link: "/admin/connectors/linear",
},
{
name: (
<div className="flex">
<ProductboardIcon size={16} />
<div className="ml-1">Productboard</div>
</div>
),
link: "/admin/connectors/productboard",
},
{
name: (
<div className="flex">
<SlabIcon size={16} />
<div className="ml-1">Slab</div>
</div>
),
link: "/admin/connectors/slab",
},
{
name: (
<div className="flex">
<NotionIcon size={16} />
<div className="ml-1">Notion</div>
</div>
),
link: "/admin/connectors/notion",
},
{
name: (
<div className="flex">
<GuruIcon size={16} />
<div className="ml-1">Guru</div>
</div>
),
link: "/admin/connectors/guru",
},
{
name: (
<div className="flex">
<BookstackIcon size={16} />
<div className="ml-1">BookStack</div>
</div>
),
link: "/admin/connectors/bookstack",
},
{
name: (
<div className="flex">
<ZulipIcon size={16} />
<div className="ml-1">Zulip</div>
</div>
),
link: "/admin/connectors/zulip",
},
{
name: (
<div className="flex">
<GongIcon size={16} />
<div className="ml-1">Gong</div>
</div>
),
link: "/admin/connectors/gong",
},
{
name: (
<div className="flex">
<GoogleSitesIcon size={16} />
<div className="ml-1">Google Sites</div>
</div>
),
link: "/admin/connectors/google-sites",
},
{
name: (
<div className="flex">
<GlobeIcon size={16} />
<div className="ml-1">Web</div>
</div>
),
link: "/admin/connectors/web",
},
{
name: (
<div className="flex">
<FileIcon size={16} />
<div className="ml-1">File</div>
</div>
),
link: "/admin/connectors/file",
},
{
name: (
<div className="flex">
<HubSpotIcon size={16} />
<div className="ml-1">HubSpot</div>
</div>
),
link: "/admin/connectors/hubspot",
},
{
name: (
<div className="flex">
<RequestTrackerIcon size={16} />
<div className="ml-1">Request Tracker</div>
</div>
),
link: "/admin/connectors/requesttracker",
},
{
name: (
<div className="flex">
<Document360Icon size={16} />
<div className="ml-1">Document360</div>
</div>
),
link: "/admin/connectors/document360",
},
{
name: (
<div className="flex">
<ZendeskIcon size={16} />
<div className="ml-1">Zendesk</div>
</div>
),
link: "/admin/connectors/zendesk",
},
],
},
{
name: "Keys",
items: [
{
name: (
<div className="flex">
<KeyIcon size={18} />
<div className="ml-1">OpenAI</div>
</div>
),
link: "/admin/keys/openai",
},
],
},
{
name: "User Management",
items: [
{
name: (
<div className="flex">
<UsersIcon size={18} />
<div className="ml-1">Users</div>
</div>
),
link: "/admin/users",
},
],
},
{
name: "Document Management",
items: [
{
name: (
<div className="flex">
<BookmarkIcon size={18} />
<div className="ml-1">Document Sets</div>
</div>
),
link: "/admin/documents/sets",
},
{
name: (
<div className="flex">
<ZoomInIcon size={18} />
<div className="ml-1">Explorer</div>
</div>
),
link: "/admin/documents/explorer",
},
{
name: (
<div className="flex">
<ThumbsUpIcon size={18} />
<div className="ml-1">Feedback</div>
</div>
),
link: "/admin/documents/feedback",
},
],
},
{
name: "Custom Assistants",
items: [
{
name: (
<div className="flex">
<RobotIcon size={18} />
<div className="ml-1">Personas</div>
</div>
),
link: "/admin/personas",
},
{
name: (
<div className="flex">
<CPUIcon size={18} />
<div className="ml-1">Slack Bots</div>
</div>
),
link: "/admin/bot",
},
],
},
{
name: "System Information",
items: [
{
name: (
<div className="flex">
<NotebookIcon size={18} />
<div className="ml-1">Version</div>
</div>
),
link: "/admin/systeminfo",
},
],
},
]}
/>
<div className="px-12 min-h-screen bg-gray-900 text-gray-100 w-full">
{children}
<div className="w-72">
<Sidebar
collections={[
{
name: "Connectors",
items: [
{
name: (
<div className="flex">
<NotebookIcon size={18} />
<div className="ml-1">Existing Connectors</div>
</div>
),
link: "/admin/indexing/status",
},
{
name: (
<div className="flex">
<ConnectorIcon size={18} />
<div className="ml-1.5">Add Connector</div>
</div>
),
link: "/admin/add-connector",
},
],
},
{
name: "Document Management",
items: [
{
name: (
<div className="flex">
<BookmarkIcon size={18} />
<div className="ml-1">Document Sets</div>
</div>
),
link: "/admin/documents/sets",
},
{
name: (
<div className="flex">
<ZoomInIcon size={18} />
<div className="ml-1">Explorer</div>
</div>
),
link: "/admin/documents/explorer",
},
{
name: (
<div className="flex">
<ThumbsUpIcon size={18} />
<div className="ml-1">Feedback</div>
</div>
),
link: "/admin/documents/feedback",
},
],
},
{
name: "Custom Assistants",
items: [
{
name: (
<div className="flex">
<RobotIcon size={18} />
<div className="ml-1">Personas</div>
</div>
),
link: "/admin/personas",
},
{
name: (
<div className="flex">
<CPUIcon size={18} />
<div className="ml-1">Slack Bots</div>
</div>
),
link: "/admin/bot",
},
],
},
{
name: "Keys",
items: [
{
name: (
<div className="flex">
<KeyIcon size={18} />
<div className="ml-1">OpenAI</div>
</div>
),
link: "/admin/keys/openai",
},
],
},
{
name: "User Management",
items: [
{
name: (
<div className="flex">
<UsersIcon size={18} />
<div className="ml-1">Users</div>
</div>
),
link: "/admin/users",
},
],
},
]}
/>
</div>
<div className="px-12 bg-gray-900 text-gray-100 w-full">{children}</div>
</div>
</div>
);

View File

@@ -4,19 +4,26 @@ import { Divider } from "@tremor/react";
export function AdminPageTitle({
icon,
title,
farRightElement,
includeDivider = true,
}: {
icon: JSX.Element;
title: string | JSX.Element;
farRightElement?: JSX.Element;
includeDivider?: boolean;
}) {
return (
<div className="dark">
<div className="mb-4">
<HealthCheckBanner />
</div>
<h1 className="text-3xl font-bold flex gap-x-2 mb-2">
{icon} {title}
</h1>
<Divider />
<div className="flex">
<h1 className="text-3xl font-bold flex gap-x-2">
{icon} {title}
</h1>
{farRightElement && <div className="ml-auto">{farRightElement}</div>}
</div>
{includeDivider && <Divider />}
</div>
);
}

View File

@@ -1,13 +1,11 @@
import { getSourceMetadata } from "@/components/source";
import { getSourceMetadata } from "@/lib/sources";
import {
ConfluenceConfig,
Connector,
ConnectorIndexingStatus,
GithubConfig,
GoogleDriveConfig,
JiraConfig,
SlackConfig,
WebConfig,
ZulipConfig,
} from "@/lib/types";
import Link from "next/link";
@@ -96,7 +94,7 @@ export const ConnectorTitle = ({
</>
);
return (
<div>
<div className="my-auto">
{isLink ? (
<Link
className={mainSectionClassName}
@@ -107,7 +105,7 @@ export const ConnectorTitle = ({
) : (
<div className={mainSectionClassName}>{mainDisplay}</div>
)}
{showMetadata && (
{showMetadata && additionalMetadata.size > 0 && (
<div className="text-xs text-gray-300 mt-1">
{Array.from(additionalMetadata.entries()).map(([key, value]) => {
return (

View File

@@ -13,13 +13,12 @@ interface Collection {
}
interface SidebarProps {
title: string;
collections: Collection[];
}
export const Sidebar: React.FC<SidebarProps> = ({ collections }) => {
export function Sidebar({ collections }: SidebarProps) {
return (
<aside className="w-64 bg-gray-900 text-gray-100 pl-4">
<aside className="bg-gray-900 text-gray-100 pl-4">
<nav className="space-y-2 pl-4">
{collections.map((collection, collectionInd) => (
<div key={collectionInd}>
@@ -40,4 +39,4 @@ export const Sidebar: React.FC<SidebarProps> = ({ collections }) => {
</nav>
</aside>
);
};
}

View File

@@ -35,6 +35,7 @@ import {
FiBookmark,
FiCpu,
FiInfo,
FiUploadCloud,
} from "react-icons/fi";
import { SiBookstack } from "react-icons/si";
import Image from "next/image";
@@ -289,6 +290,13 @@ export const RobotIcon = ({
return <FaRobot size={size} className={className} />;
};
export const ConnectorIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => {
return <FiUploadCloud size={size} className={className} />;
};
//
// COMPANY LOGOS
//

View File

@@ -1,11 +1,11 @@
import { DanswerDocument } from "@/lib/search/interfaces";
import { DocumentFeedbackBlock } from "./DocumentFeedbackBlock";
import { getSourceIcon } from "../source";
import { useState } from "react";
import { PopupSpec } from "../admin/connectors/Popup";
import { HoverPopup } from "@/components/HoverPopup";
import { DocumentUpdatedAtBadge } from "./DocumentUpdatedAtBadge";
import { FiCrosshair, FiInfo, FiRadio } from "react-icons/fi";
import { FiInfo, FiRadio } from "react-icons/fi";
import { SourceIcon } from "../SourceIcon";
export const buildDocumentSummaryDisplay = (
matchHighlights: string[],
@@ -186,7 +186,7 @@ export const DocumentDisplay = ({
target="_blank"
rel="noopener noreferrer"
>
{getSourceIcon(document.source_type, 22)}
<SourceIcon sourceType={document.source_type} iconSize={22} />
<p className="truncate break-all ml-2 my-auto text-base">
{document.semantic_identifier || document.document_id}
</p>

View File

@@ -1,35 +1,14 @@
import React from "react";
import { getSourceIcon } from "../../source";
import { DocumentSet, ValidSources } from "@/lib/types";
import { Source } from "@/lib/search/interfaces";
import { SourceMetadata } from "@/lib/search/interfaces";
import { InfoIcon, defaultTailwindCSS } from "../../icons/icons";
import { HoverPopup } from "../../HoverPopup";
import { FiBook, FiBookmark, FiFilter, FiMap, FiX } from "react-icons/fi";
import { DateRangeSelector } from "../DateRangeSelector";
import { DateRangePickerValue } from "@tremor/react";
import { FilterDropdown } from "./FilterDropdown";
const sources: Source[] = [
{ displayName: "Google Drive", internalName: "google_drive" },
{ displayName: "Slack", internalName: "slack" },
{ displayName: "BookStack", internalName: "bookstack" },
{ displayName: "Confluence", internalName: "confluence" },
{ displayName: "Jira", internalName: "jira" },
{ displayName: "Productboard", internalName: "productboard" },
{ displayName: "Slab", internalName: "slab" },
{ displayName: "Github PRs", internalName: "github" },
{ displayName: "Web", internalName: "web" },
{ displayName: "Guru", internalName: "guru" },
{ displayName: "Gong", internalName: "gong" },
{ displayName: "File", internalName: "file" },
{ displayName: "Notion", internalName: "notion" },
{ displayName: "Zulip", internalName: "zulip" },
{ displayName: "Linear", internalName: "linear" },
{ displayName: "HubSpot", internalName: "hubspot" },
{ displayName: "Document360", internalName: "document360" },
{ displayName: "Request Tracker", internalName: "requesttracker" },
{ displayName: "Google Sites", internalName: "google_sites" },
];
import { listSourceMetadata } from "@/lib/sources";
import { SourceIcon } from "@/components/SourceIcon";
const SectionTitle = ({ children }: { children: string }) => (
<div className="font-medium text-sm flex">{children}</div>
@@ -40,8 +19,8 @@ interface SourceSelectorProps {
setTimeRange: React.Dispatch<
React.SetStateAction<DateRangePickerValue | null>
>;
selectedSources: Source[];
setSelectedSources: React.Dispatch<React.SetStateAction<Source[]>>;
selectedSources: SourceMetadata[];
setSelectedSources: React.Dispatch<React.SetStateAction<SourceMetadata[]>>;
selectedDocumentSets: string[];
setSelectedDocumentSets: React.Dispatch<React.SetStateAction<string[]>>;
availableDocumentSets: DocumentSet[];
@@ -58,8 +37,8 @@ export function SourceSelector({
availableDocumentSets,
existingSources,
}: SourceSelectorProps) {
const handleSelect = (source: Source) => {
setSelectedSources((prev: Source[]) => {
const handleSelect = (source: SourceMetadata) => {
setSelectedSources((prev: SourceMetadata[]) => {
if (prev.includes(source)) {
return prev.filter((s) => s.internalName !== source.internalName);
} else {
@@ -96,7 +75,7 @@ export function SourceSelector({
<div className="mt-4">
<SectionTitle>Sources</SectionTitle>
<div className="px-1">
{sources
{listSourceMetadata()
.filter((source) => existingSources.includes(source.internalName))
.map((source) => (
<div
@@ -110,7 +89,7 @@ export function SourceSelector({
}
onClick={() => handleSelect(source)}
>
{getSourceIcon(source.internalName, 16)}
<SourceIcon sourceType={source.internalName} iconSize={16} />
<span className="ml-2 text-sm text-gray-200">
{source.displayName}
</span>
@@ -201,8 +180,8 @@ export function HorizontalFilters({
availableDocumentSets,
existingSources,
}: SourceSelectorProps) {
const handleSourceSelect = (source: Source) => {
setSelectedSources((prev: Source[]) => {
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);
@@ -222,7 +201,8 @@ export function HorizontalFilters({
});
};
const availableSources = sources.filter((source) =>
const allSources = listSourceMetadata();
const availableSources = allSources.filter((source) =>
existingSources.includes(source.internalName)
);
@@ -239,8 +219,7 @@ export function HorizontalFilters({
key: source.displayName,
display: (
<>
{" "}
{getSourceIcon(source.internalName, 16)}
<SourceIcon sourceType={source.internalName} iconSize={16} />
<span className="ml-2 text-sm text-gray-200">
{source.displayName}
</span>
@@ -251,7 +230,7 @@ export function HorizontalFilters({
selected={selectedSources.map((source) => source.displayName)}
handleSelect={(option) =>
handleSourceSelect(
sources.find((source) => source.displayName === option.key)!
allSources.find((source) => source.displayName === option.key)!
)
}
icon={
@@ -305,7 +284,7 @@ export function HorizontalFilters({
onClick={() => handleSourceSelect(source)}
>
<>
{getSourceIcon(source.internalName, 16)}
<SourceIcon sourceType={source.internalName} iconSize={16} />
<span className="ml-2 text-sm text-gray-400">
{source.displayName}
</span>

View File

@@ -1,8 +1,8 @@
import { Quote } from "@/lib/search/interfaces";
import { ResponseSection, StatusOptions } from "./ResponseSection";
import { getSourceIcon } from "@/components/source";
import { CheckmarkIcon, CopyIcon } from "@/components/icons/icons";
import { useState } from "react";
import { SourceIcon } from "@/components/SourceIcon";
const QuoteDisplay = ({ quoteInfo }: { quoteInfo: Quote }) => {
const [detailIsOpen, setDetailIsOpen] = useState(false);
@@ -54,7 +54,7 @@ const QuoteDisplay = ({ quoteInfo }: { quoteInfo: Quote }) => {
target="_blank"
rel="noopener noreferrer"
>
{getSourceIcon(quoteInfo.source_type, 20)}
<SourceIcon sourceType={quoteInfo.source_type} iconSize={20} />
<p className="truncate break-all ml-2 mr-2">
{quoteInfo.semantic_identifier || quoteInfo.document_id}
</p>

View File

@@ -1,168 +0,0 @@
import { ValidSources } from "@/lib/types";
import {
BookstackIcon,
ConfluenceIcon,
FileIcon,
GithubIcon,
GlobeIcon,
GoogleDriveIcon,
GuruIcon,
GongIcon,
JiraIcon,
LinearIcon,
NotionIcon,
ProductboardIcon,
SlabIcon,
SlackIcon,
ZulipIcon,
HubSpotIcon,
Document360Icon,
GoogleSitesIcon,
RequestTrackerIcon,
ZendeskIcon,
} from "./icons/icons";
interface SourceMetadata {
icon: React.FC<{ size?: number; className?: string }>;
displayName: string;
adminPageLink: string;
}
export const getSourceMetadata = (sourceType: ValidSources): SourceMetadata => {
switch (sourceType) {
case "web":
return {
icon: GlobeIcon,
displayName: "Web",
adminPageLink: "/admin/connectors/web",
};
case "file":
return {
icon: FileIcon,
displayName: "File",
adminPageLink: "/admin/connectors/file",
};
case "slack":
return {
icon: SlackIcon,
displayName: "Slack",
adminPageLink: "/admin/connectors/slack",
};
case "google_drive":
return {
icon: GoogleDriveIcon,
displayName: "Google Drive",
adminPageLink: "/admin/connectors/google-drive",
};
case "github":
return {
icon: GithubIcon,
displayName: "Github PRs",
adminPageLink: "/admin/connectors/github",
};
case "bookstack":
return {
icon: BookstackIcon,
displayName: "BookStack",
adminPageLink: "/admin/connectors/bookstack",
};
case "confluence":
return {
icon: ConfluenceIcon,
displayName: "Confluence",
adminPageLink: "/admin/connectors/confluence",
};
case "jira":
return {
icon: JiraIcon,
displayName: "Jira",
adminPageLink: "/admin/connectors/jira",
};
case "productboard":
return {
icon: ProductboardIcon,
displayName: "Productboard",
adminPageLink: "/admin/connectors/productboard",
};
case "slab":
return {
icon: SlabIcon,
displayName: "Slab",
adminPageLink: "/admin/connectors/slab",
};
case "notion":
return {
icon: NotionIcon,
displayName: "Notion",
adminPageLink: "/admin/connectors/notion",
};
case "zulip":
return {
icon: ZulipIcon,
displayName: "Zulip",
adminPageLink: "/admin/connectors/zulip",
};
case "guru":
return {
icon: GuruIcon,
displayName: "Guru",
adminPageLink: "/admin/connectors/guru",
};
case "gong":
return {
icon: GongIcon,
displayName: "Gong",
adminPageLink: "/admin/connectors/gong",
};
case "linear":
return {
icon: LinearIcon,
displayName: "Linear",
adminPageLink: "/admin/connectors/linear",
};
case "hubspot":
return {
icon: HubSpotIcon,
displayName: "HubSpot",
adminPageLink: "/admin/connectors/hubspot",
};
case "document360":
return {
icon: Document360Icon,
displayName: "Document360",
adminPageLink: "/admin/connectors/document360",
};
case "requesttracker":
return {
icon: RequestTrackerIcon,
displayName: "Request Tracker",
adminPageLink: "/admin/connectors/requesttracker",
};
case "google_sites":
return {
icon: GoogleSitesIcon,
displayName: "Google Sites",
adminPageLink: "/admin/connectors/google-sites",
};
case "zendesk":
return {
icon: ZendeskIcon,
displayName: "Zendesk",
adminPageLink: "/admin/connectors/zendesk",
};
default:
throw new Error("Invalid source type");
}
};
export const getSourceIcon = (sourceType: ValidSources, iconSize: number) => {
return getSourceMetadata(sourceType).icon({
size: iconSize,
});
};
export const getSourceDisplayName = (
sourceType: ValidSources
): string | null => {
return getSourceMetadata(sourceType).displayName;
};

View File

@@ -7,7 +7,7 @@ import useSWR, { mutate, useSWRConfig } from "swr";
import { fetcher } from "./fetcher";
import { useState } from "react";
import { DateRangePickerValue } from "@tremor/react";
import { Source } from "./search/interfaces";
import { SourceMetadata } from "./search/interfaces";
const CREDENTIAL_URL = "/api/manage/admin/credential";
@@ -77,7 +77,7 @@ export const useTimeRange = (initialValue?: DateRangePickerValue) => {
export function useFilters() {
const [timeRange, setTimeRange] = useTimeRange();
const [selectedSources, setSelectedSources] = useState<Source[]>([]);
const [selectedSources, setSelectedSources] = useState<SourceMetadata[]>([]);
const [selectedDocumentSets, setSelectedDocumentSets] = useState<string[]>(
[]
);

View File

@@ -74,9 +74,18 @@ export interface SearchResponse {
queryEventId: number | null;
}
export interface Source {
export enum SourceCategory {
AppConnection = "Connect to Apps",
ImportedKnowledge = "Import Knowledge",
}
export interface SourceMetadata {
icon: React.FC<{ size?: number; className?: string }>;
displayName: string;
category: SourceCategory;
shortDescription?: string;
internalName: ValidSources;
adminUrl: string;
}
export interface SearchDefaultOverrides {
@@ -93,7 +102,7 @@ export interface Filters {
export interface SearchRequestArgs {
query: string;
chatSessionId: number;
sources: Source[];
sources: SourceMetadata[];
documentSets: string[];
timeRange: DateRangePickerValue | null;
updateCurrentAnswer: (val: string) => void;

View File

@@ -1,8 +1,8 @@
import { Filters, Source } from "./interfaces";
import { Filters, SourceMetadata } from "./interfaces";
import { DateRangePickerValue } from "@tremor/react";
export const buildFilters = (
sources: Source[],
sources: SourceMetadata[],
documentSets: string[],
timeRange: DateRangePickerValue | null
): Filters => {

164
web/src/lib/sources.ts Normal file
View File

@@ -0,0 +1,164 @@
import {
BookstackIcon,
ConfluenceIcon,
Document360Icon,
FileIcon,
GithubIcon,
GlobeIcon,
GongIcon,
GoogleDriveIcon,
GoogleSitesIcon,
GuruIcon,
HubSpotIcon,
JiraIcon,
LinearIcon,
NotionIcon,
ProductboardIcon,
RequestTrackerIcon,
SlabIcon,
SlackIcon,
ZendeskIcon,
ZulipIcon,
} from "@/components/icons/icons";
import { ValidSources } from "./types";
import { SourceCategory, SourceMetadata } from "./search/interfaces";
interface PartialSourceMetadata {
icon: React.FC<{ size?: number; className?: string }>;
displayName: string;
category: SourceCategory;
}
type SourceMap = {
[K in ValidSources]: PartialSourceMetadata;
};
const SOURCE_METADATA_MAP: SourceMap = {
web: {
icon: GlobeIcon,
displayName: "Web",
category: SourceCategory.ImportedKnowledge,
},
file: {
icon: FileIcon,
displayName: "File",
category: SourceCategory.ImportedKnowledge,
},
slack: {
icon: SlackIcon,
displayName: "Slack",
category: SourceCategory.AppConnection,
},
google_drive: {
icon: GoogleDriveIcon,
displayName: "Google Drive",
category: SourceCategory.AppConnection,
},
github: {
icon: GithubIcon,
displayName: "Github",
category: SourceCategory.AppConnection,
},
confluence: {
icon: ConfluenceIcon,
displayName: "Confluence",
category: SourceCategory.AppConnection,
},
jira: {
icon: JiraIcon,
displayName: "Jira",
category: SourceCategory.AppConnection,
},
notion: {
icon: NotionIcon,
displayName: "Notion",
category: SourceCategory.AppConnection,
},
zendesk: {
icon: ZendeskIcon,
displayName: "Zendesk",
category: SourceCategory.AppConnection,
},
gong: {
icon: GongIcon,
displayName: "Gong",
category: SourceCategory.AppConnection,
},
linear: {
icon: LinearIcon,
displayName: "Linear",
category: SourceCategory.AppConnection,
},
productboard: {
icon: ProductboardIcon,
displayName: "Productboard",
category: SourceCategory.AppConnection,
},
slab: {
icon: SlabIcon,
displayName: "Slab",
category: SourceCategory.AppConnection,
},
zulip: {
icon: ZulipIcon,
displayName: "Zulip",
category: SourceCategory.AppConnection,
},
guru: {
icon: GuruIcon,
displayName: "Guru",
category: SourceCategory.AppConnection,
},
hubspot: {
icon: HubSpotIcon,
displayName: "HubSpot",
category: SourceCategory.AppConnection,
},
document360: {
icon: Document360Icon,
displayName: "Document360",
category: SourceCategory.AppConnection,
},
bookstack: {
icon: BookstackIcon,
displayName: "BookStack",
category: SourceCategory.AppConnection,
},
google_sites: {
icon: GoogleSitesIcon,
displayName: "Google Sites",
category: SourceCategory.ImportedKnowledge,
},
requesttracker: {
icon: RequestTrackerIcon,
displayName: "Request Tracker",
category: SourceCategory.AppConnection,
},
};
function fillSourceMetadata(
partialMetadata: PartialSourceMetadata,
internalName: ValidSources
): SourceMetadata {
return {
internalName: internalName,
...partialMetadata,
adminUrl: `/admin/connectors/${partialMetadata.displayName
.toLowerCase()
.replaceAll(" ", "-")}`,
};
}
export function getSourceMetadata(sourceType: ValidSources): SourceMetadata {
return fillSourceMetadata(SOURCE_METADATA_MAP[sourceType], sourceType);
}
export function listSourceMetadata(): SourceMetadata[] {
return Object.entries(SOURCE_METADATA_MAP).map(([source, metadata]) => {
return fillSourceMetadata(metadata, source as ValidSources);
});
}
export function getSourceDisplayName(sourceType: ValidSources): string | null {
return getSourceMetadata(sourceType).displayName;
}