Improve initial flow

This commit is contained in:
Weves
2024-02-17 15:25:27 -08:00
committed by Chris Weaver
parent f9733f9870
commit 6059339e61
23 changed files with 725 additions and 211 deletions

View File

@@ -481,9 +481,7 @@ export const Chat = ({
}
};
const retrievalDisabled = selectedPersona
? !personaIncludesRetrieval(selectedPersona)
: false;
const retrievalDisabled = !personaIncludesRetrieval(livePersona);
return (
<div className="flex w-full overflow-x-hidden" ref={masterFlexboxRef}>

View File

@@ -8,7 +8,6 @@ import { DocumentSet, Tag, User, ValidSources } from "@/lib/types";
import { Persona } from "../admin/personas/interfaces";
import { Header } from "@/components/Header";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
export function ChatLayout({
@@ -44,7 +43,6 @@ export function ChatLayout({
<Header user={user} />
</div>
<HealthCheckBanner />
<ApiKeyModal />
<InstantSSRAutoRefresh />
<div className="flex relative bg-background text-default overflow-x-hidden">

View File

@@ -5,12 +5,21 @@ import {
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, Tag, User, ValidSources } from "@/lib/types";
import {
CCPairBasicInfo,
DocumentSet,
Tag,
User,
ValidSources,
} from "@/lib/types";
import { ChatSession } 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 {
WelcomeModal,
hasCompletedWelcomeFlowSS,
} from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
import { cookies } from "next/headers";
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/contants";
@@ -21,6 +30,8 @@ import {
checkModelNameIsValid,
} from "../admin/models/embedding/embeddingModels";
import { SwitchModelModal } from "@/components/SwitchModelModal";
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
export default async function Page({
searchParams,
@@ -32,7 +43,7 @@ export default async function Page({
const tasks = [
getAuthTypeMetadataSS(),
getCurrentUserSS(),
fetchSS("/manage/connector"),
fetchSS("/manage/indexing-status"),
fetchSS("/manage/document-set"),
fetchSS("/persona?include_default=true"),
fetchSS("/chat/get-user-chat-sessions"),
@@ -57,7 +68,7 @@ export default async function Page({
}
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
const user = results[1] as User | null;
const connectorsResponse = results[2] as Response | null;
const ccPairsResponse = 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;
@@ -73,16 +84,16 @@ export default async function Page({
return redirect("/auth/waiting-on-verification");
}
let connectors: Connector<any>[] = [];
if (connectorsResponse?.ok) {
connectors = await connectorsResponse.json();
let ccPairs: CCPairBasicInfo[] = [];
if (ccPairsResponse?.ok) {
ccPairs = await ccPairsResponse.json();
} else {
console.log(`Failed to fetch connectors - ${connectorsResponse?.status}`);
console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`);
}
const availableSources: ValidSources[] = [];
connectors.forEach((connector) => {
if (!availableSources.includes(connector.source)) {
availableSources.push(connector.source);
ccPairs.forEach((ccPair) => {
if (!availableSources.includes(ccPair.source)) {
availableSources.push(ccPair.source);
}
});
@@ -145,19 +156,31 @@ export default async function Page({
? parseInt(documentSidebarCookieInitialWidth.value)
: undefined;
const shouldShowWelcomeModal = !hasCompletedWelcomeFlowSS();
const hasAnyConnectors = ccPairs.length > 0;
const shouldDisplaySourcesIncompleteModal =
hasAnyConnectors &&
!shouldShowWelcomeModal &&
!ccPairs.some(
(ccPair) => ccPair.has_successful_run && ccPair.docs_indexed > 0
);
// if no connectors are setup, only show personas that are pure
// passthrough and don't do any retrieval
if (!hasAnyConnectors) {
personas = personas.filter((persona) => persona.num_chunks === 0);
}
return (
<>
<InstantSSRAutoRefresh />
<ApiKeyModal />
{connectors.length === 0 ? (
<WelcomeModal embeddingModelName={currentEmbeddingModelName} />
) : (
embeddingModelVersionInfo &&
!checkModelNameIsValid(currentEmbeddingModelName) &&
!nextEmbeddingModelName && (
<SwitchModelModal embeddingModelName={currentEmbeddingModelName} />
)
{shouldShowWelcomeModal && <WelcomeModal />}
{!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && (
<ApiKeyModal />
)}
{shouldDisplaySourcesIncompleteModal && (
<NoCompleteSourcesModal ccPairs={ccPairs} />
)}
<ChatLayout

View File

@@ -9,19 +9,20 @@ import { redirect } from "next/navigation";
import { HealthCheckBanner } from "@/components/health/healthcheck";
import { ApiKeyModal } from "@/components/openai/ApiKeyModal";
import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, Tag, User, ValidSources } from "@/lib/types";
import { CCPairBasicInfo, DocumentSet, Tag, 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";
import {
WelcomeModal,
hasCompletedWelcomeFlowSS,
} from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { personaComparator } from "../admin/personas/lib";
import {
FullEmbeddingModelResponse,
checkModelNameIsValid,
} from "../admin/models/embedding/embeddingModels";
import { SwitchModelModal } from "@/components/SwitchModelModal";
import { FullEmbeddingModelResponse } from "../admin/models/embedding/embeddingModels";
import { NoSourcesModal } from "@/components/initialSetup/search/NoSourcesModal";
import { NoCompleteSourcesModal } from "@/components/initialSetup/search/NoCompleteSourceModal";
export default async function Home() {
// Disable caching so we always get the up to date connector / document set / persona info
@@ -32,7 +33,7 @@ export default async function Home() {
const tasks = [
getAuthTypeMetadataSS(),
getCurrentUserSS(),
fetchSS("/manage/connector"),
fetchSS("/manage/indexing-status"),
fetchSS("/manage/document-set"),
fetchSS("/persona"),
fetchSS("/query/valid-tags"),
@@ -56,7 +57,7 @@ export default async function Home() {
}
const authTypeMetadata = results[0] as AuthTypeMetadata | null;
const user = results[1] as User | null;
const connectorsResponse = results[2] as Response | null;
const ccPairsResponse = results[2] as Response | null;
const documentSetsResponse = results[3] as Response | null;
const personaResponse = results[4] as Response | null;
const tagsResponse = results[5] as Response | null;
@@ -71,11 +72,11 @@ export default async function Home() {
return redirect("/auth/waiting-on-verification");
}
let connectors: Connector<any>[] = [];
if (connectorsResponse?.ok) {
connectors = await connectorsResponse.json();
let ccPairs: CCPairBasicInfo[] = [];
if (ccPairsResponse?.ok) {
ccPairs = await ccPairsResponse.json();
} else {
console.log(`Failed to fetch connectors - ${connectorsResponse?.status}`);
console.log(`Failed to fetch connectors - ${ccPairsResponse?.status}`);
}
let documentSets: DocumentSet[] = [];
@@ -126,29 +127,37 @@ export default async function Home() {
? (storedSearchType as SearchType)
: SearchType.SEMANTIC; // default to semantic
const shouldShowWelcomeModal = !hasCompletedWelcomeFlowSS();
const shouldDisplayNoSourcesModal =
ccPairs.length === 0 && !shouldShowWelcomeModal;
const shouldDisplaySourcesIncompleteModal =
!ccPairs.some(
(ccPair) => ccPair.has_successful_run && ccPair.docs_indexed > 0
) &&
!shouldDisplayNoSourcesModal &&
!shouldShowWelcomeModal;
return (
<>
<Header user={user} />
<div className="m-3">
<HealthCheckBanner />
</div>
<ApiKeyModal />
<InstantSSRAutoRefresh />
{connectors.length === 0 ? (
<WelcomeModal embeddingModelName={currentEmbeddingModelName} />
) : (
embeddingModelVersionInfo &&
!checkModelNameIsValid(currentEmbeddingModelName) &&
!nextEmbeddingModelName && (
<SwitchModelModal embeddingModelName={currentEmbeddingModelName} />
)
{shouldShowWelcomeModal && <WelcomeModal />}
{!shouldShowWelcomeModal &&
!shouldDisplayNoSourcesModal &&
!shouldDisplaySourcesIncompleteModal && <ApiKeyModal />}
{shouldDisplayNoSourcesModal && <NoSourcesModal />}
{shouldDisplaySourcesIncompleteModal && (
<NoCompleteSourcesModal ccPairs={ccPairs} />
)}
<InstantSSRAutoRefresh />
<div className="px-24 pt-10 flex flex-col items-center min-h-screen">
<div className="w-full">
<SearchSection
connectors={connectors}
ccPairs={ccPairs}
documentSets={documentSets}
personas={personas}
tags={tags}

View File

@@ -4,7 +4,11 @@ import { useRouter } from "next/navigation";
import { FiChevronLeft } from "react-icons/fi";
export function BackButton() {
export function BackButton({
behaviorOverride,
}: {
behaviorOverride?: () => void;
}) {
const router = useRouter();
return (
@@ -20,7 +24,13 @@ export function BackButton() {
cursor-pointer
rounded-lg
text-sm`}
onClick={() => router.back()}
onClick={() => {
if (behaviorOverride) {
behaviorOverride();
} else {
router.back();
}
}}
>
<FiChevronLeft className="mr-1 my-auto" />
Back

View File

@@ -7,6 +7,8 @@ interface ModalProps {
onOutsideClick?: () => void;
className?: string;
width?: string;
titleSize?: string;
hideDividerForTitle?: boolean;
}
export function Modal({
@@ -15,6 +17,8 @@ export function Modal({
onOutsideClick,
className,
width,
titleSize,
hideDividerForTitle,
}: ModalProps) {
return (
<div>
@@ -36,7 +40,11 @@ export function Modal({
{title && (
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">{title}</h2>
<h2
className={"my-auto font-bold " + (titleSize || "text-2xl")}
>
{title}
</h2>
{onOutsideClick && (
<div
onClick={onOutsideClick}
@@ -46,7 +54,7 @@ export function Modal({
</div>
)}
</div>
<Divider />
{!hideDividerForTitle && <Divider />}
</>
)}
{children}

View File

@@ -1,71 +0,0 @@
"use client";
import { Button, Text } from "@tremor/react";
import { Modal } from "./Modal";
import Link from "next/link";
import { FiCheckCircle } from "react-icons/fi";
import { checkModelNameIsValid } from "@/app/admin/models/embedding/embeddingModels";
export function WelcomeModal({
embeddingModelName,
}: {
embeddingModelName: undefined | null | string;
}) {
const validModelSelected = checkModelNameIsValid(embeddingModelName);
return (
<Modal className="max-w-4xl">
<div className="text-base">
<h2 className="text-xl font-bold mb-4 pb-2 border-b border-border flex">
Welcome to Danswer 🎉
</h2>
<div>
<p>
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>
</div>
<div className="flex mt-8 mb-2">
{validModelSelected && (
<FiCheckCircle className="my-auto mr-2 text-success" />
)}
<Text className="font-bold">Step 1: Choose Your Embedding Model</Text>
</div>
{!validModelSelected && (
<>
To get started, the first step is to choose your{" "}
<i>embedding model</i>. This machine learning model helps power
Danswer&apos;s search. Different models have different strengths,
but don&apos;t worry we&apos;ll guide you through the process of
choosing the right one for your organization.
</>
)}
<div className="flex mt-3">
<Link href="/admin/models/embedding">
<Button size="xs">
{validModelSelected
? "Change your Embedding Model"
: "Choose your Embedding Model"}
</Button>
</Link>
</div>
<Text className="font-bold mt-8 mb-2">
Step 2: Add Your First Connector
</Text>
Next, we need to 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.
<div className="flex mt-3">
<Link href="/admin/add-connector">
<Button size="xs" disabled={!validModelSelected}>
Setup your first connector!
</Button>
</Link>
</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,71 @@
"use client";
import { Modal } from "../../Modal";
import Link from "next/link";
import { useEffect, useState } from "react";
import { CCPairBasicInfo } from "@/lib/types";
import { useRouter } from "next/navigation";
export function NoCompleteSourcesModal({
ccPairs,
}: {
ccPairs: CCPairBasicInfo[];
}) {
const router = useRouter();
const [isHidden, setIsHidden] = useState(false);
useEffect(() => {
const interval = setInterval(() => {
router.refresh();
}, 5000);
return () => clearInterval(interval);
}, []);
if (isHidden) {
return null;
}
const totalDocs = ccPairs.reduce(
(acc, ccPair) => acc + ccPair.docs_indexed,
0
);
return (
<Modal
className="max-w-4xl"
title="⏳ None of your connectors have finished a full sync yet"
onOutsideClick={() => setIsHidden(true)}
>
<div className="text-base">
<div>
<div>
You&apos;ve connected some sources, but none of them have finished
syncing. Depending on the size of the knowledge base(s) you&apos;ve
connected to Danswer, it can take anywhere between 30 seconds to a
few days for the initial sync to complete. So far we&apos;ve synced{" "}
<b>{totalDocs}</b> documents.
<br />
<br />
To view the status of your syncing connectors, head over to the{" "}
<Link className="text-link" href="admin/indexing/status">
Existing Connectors page
</Link>
.
<br />
<br />
<p
className="text-link cursor-pointer inline"
onClick={() => {
setIsHidden(true);
}}
>
Or, click here to continue and ask questions on the partially
synced knowledge set.
</p>
</div>
</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,51 @@
"use client";
import { Button, Divider } from "@tremor/react";
import { Modal } from "../../Modal";
import Link from "next/link";
import { FiMessageSquare, FiShare2 } from "react-icons/fi";
import { useState } from "react";
export function NoSourcesModal() {
const [isHidden, setIsHidden] = useState(false);
if (isHidden) {
return null;
}
return (
<Modal
className="max-w-4xl"
title="🧐 No sources connected"
onOutsideClick={() => setIsHidden(true)}
>
<div className="text-base">
<div>
<p>
Before using Search you&apos;ll need to connect at least one source.
Without any connected knowledge sources, there isn&apos;t anything
to search over.
</p>
<Link href="/admin/add-connector">
<Button className="mt-3" size="xs" icon={FiShare2}>
Connect a Source!
</Button>
</Link>
<Divider />
<div>
<p>
Or, if you&apos;re looking for a pure ChatGPT-like experience
without any organization specific knowledge, then you can head
over to the Chat page and start chatting with Danswer right away!
</p>
<Link href="/chat">
<Button className="mt-3" size="xs" icon={FiMessageSquare}>
Start Chatting!
</Button>
</Link>
</div>
</div>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,287 @@
"use client";
import { Button, Divider, Text } from "@tremor/react";
import { Modal } from "../../Modal";
import Link from "next/link";
import Cookies from "js-cookie";
import { useRouter } from "next/navigation";
import { COMPLETED_WELCOME_FLOW_COOKIE } from "./constants";
import { FiCheckCircle, FiMessageSquare, FiShare2 } from "react-icons/fi";
import { useEffect, useState } from "react";
import { BackButton } from "@/components/BackButton";
import { ApiKeyForm } from "@/components/openai/ApiKeyForm";
import { checkApiKey } from "@/components/openai/ApiKeyModal";
function setWelcomeFlowComplete() {
Cookies.set(COMPLETED_WELCOME_FLOW_COOKIE, "true", { expires: 365 });
}
export function _CompletedWelcomeFlowDummyComponent() {
setWelcomeFlowComplete();
return null;
}
function UsageTypeSection({
title,
description,
callToAction,
icon,
onClick,
}: {
title: string;
description: string | JSX.Element;
callToAction: string;
icon?: React.ElementType;
onClick: () => void;
}) {
return (
<div>
<Text className="font-bold">{title}</Text>
<div className="text-base mt-1 mb-3">{description}</div>
<div
onClick={(e) => {
e.preventDefault();
onClick();
}}
>
<div className="text-link font-medium cursor-pointer select-none">
{callToAction}
</div>
</div>
</div>
);
}
export function _WelcomeModal() {
const router = useRouter();
const [selectedFlow, setSelectedFlow] = useState<null | "search" | "chat">(
null
);
const [isHidden, setIsHidden] = useState(false);
const [apiKeyVerified, setApiKeyVerified] = useState<boolean>(false);
useEffect(() => {
checkApiKey().then((error) => {
if (!error) {
setApiKeyVerified(true);
}
});
}, []);
if (isHidden) {
return null;
}
let title;
let body;
switch (selectedFlow) {
case "search":
title = undefined;
body = (
<>
<BackButton behaviorOverride={() => setSelectedFlow(null)} />
<div className="mt-3">
<Text className="font-bold mt-6 mb-2 flex">
{apiKeyVerified && (
<FiCheckCircle className="my-auto mr-2 text-success" />
)}
Step 1: Provide OpenAI API Key
</Text>
<div>
{apiKeyVerified ? (
<div>
API Key setup complete!
<br /> <br />
If you want to change the key later, you&apos;ll be able to
easily to do so in the Admin Panel.
</div>
) : (
<ApiKeyForm
handleResponse={async (response) => {
if (response.ok) {
setApiKeyVerified(true);
}
}}
/>
)}
</div>
<Text className="font-bold mt-6 mb-2">
Step 2: Connect Data Sources
</Text>
<div>
<p>
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 through all of them in one place.
</p>
<div className="flex mt-3">
<Link
href="/admin/add-connector"
onClick={(e) => {
e.preventDefault();
setWelcomeFlowComplete();
router.push("/admin/add-connector");
}}
className="w-fit mx-auto"
>
<Button size="xs" icon={FiShare2} disabled={!apiKeyVerified}>
Setup your first connector!
</Button>
</Link>
</div>
</div>
</div>
</>
);
break;
case "chat":
title = undefined;
body = (
<>
<BackButton behaviorOverride={() => setSelectedFlow(null)} />
<div className="mt-3">
<div>
To start using Danswer as a secure ChatGPT, we just need to
configure our LLM!
<br />
<br />
Danswer supports connections with a wide range of LLMs, including
self-hosted open-source LLMs. For more details, check out the{" "}
<a
className="text-link"
href="https://docs.danswer.dev/gen_ai_configs/overview"
>
documentation
</a>
.
<br />
<br />
If you haven&apos;t done anything special with the Gen AI configs,
then we default to use OpenAI.
</div>
<Text className="font-bold mt-6 mb-2 flex">
{apiKeyVerified && (
<FiCheckCircle className="my-auto mr-2 text-success" />
)}
Step 1: Provide LLM API Key
</Text>
<div>
{apiKeyVerified ? (
<div>
LLM setup complete!
<br /> <br />
If you want to change the key later or choose a different LLM,
you&apos;ll be able to easily to do so in the Admin Panel / by
changing some environment variables.
</div>
) : (
<ApiKeyForm
handleResponse={async (response) => {
if (response.ok) {
setApiKeyVerified(true);
}
}}
/>
)}
</div>
<Text className="font-bold mt-6 mb-2 flex">
Step 2: Start Chatting!
</Text>
<div>
Click the button below to start chatting with the LLM setup above!
Don&apos;t worry, if you do decide later on you want to connect
your organization&apos;s knowledge, you can always do that in the{" "}
<Link
className="text-link"
href="/admin/add-connector"
onClick={(e) => {
e.preventDefault();
setWelcomeFlowComplete();
router.push("/admin/add-connector");
}}
>
Admin Panel
</Link>
.
</div>
<div className="flex mt-3">
<Link
href="/chat"
onClick={(e) => {
e.preventDefault();
setWelcomeFlowComplete();
router.push("/chat");
setIsHidden(true);
}}
className="w-fit mx-auto"
>
<Button size="xs" icon={FiShare2} disabled={!apiKeyVerified}>
Start chatting!
</Button>
</Link>
</div>
</div>
</>
);
break;
default:
title = "🎉 Welcome to Danswer";
body = (
<>
<div>
<p>How are you planning on using Danswer?</p>
</div>
<Divider />
<UsageTypeSection
title="Search / Chat with Knowledge"
description={
<div>
If you&apos;re looking to search through, chat with, or ask
direct questions of your organization&apos;s knowledge, then
this is the option for you!
</div>
}
callToAction="Get Started"
onClick={() => setSelectedFlow("search")}
/>
<Divider />
<UsageTypeSection
title="Secure ChatGPT"
description={
<>
If you&apos;re looking for a pure ChatGPT-like experience, then
this is the option for you!
</>
}
icon={FiMessageSquare}
callToAction="Get Started"
onClick={() => {
setSelectedFlow("chat");
}}
/>
{/* TODO: add a Slack option here */}
{/* <Divider />
<UsageTypeSection
title="AI-powered Slack Assistant"
description="If you're looking to setup a bot to auto-answer questions in Slack"
callToAction="Connect your company knowledge!"
link="/admin/add-connector"
/> */}
</>
);
}
return (
<Modal title={title} className="max-w-4xl">
<div className="text-base">{body}</div>
</Modal>
);
}

View File

@@ -0,0 +1,23 @@
import { cookies } from "next/headers";
import {
_CompletedWelcomeFlowDummyComponent,
_WelcomeModal,
} from "./WelcomeModal";
import { COMPLETED_WELCOME_FLOW_COOKIE } from "./constants";
export function hasCompletedWelcomeFlowSS() {
const cookieStore = cookies();
return (
cookieStore.get(COMPLETED_WELCOME_FLOW_COOKIE)?.value?.toLowerCase() ===
"true"
);
}
export function WelcomeModal() {
const hasCompletedWelcomeFlow = hasCompletedWelcomeFlowSS();
if (hasCompletedWelcomeFlow) {
return <_CompletedWelcomeFlowDummyComponent />;
}
return <_WelcomeModal />;
}

View File

@@ -0,0 +1 @@
export const COMPLETED_WELCOME_FLOW_COOKIE = "completed_welcome_flow";

View File

@@ -51,25 +51,24 @@ export const ApiKeyForm = ({ handleResponse }: Props) => {
}
setTimeout(() => {
setPopup(null);
}, 4000);
}, 10000);
}
}}
>
{({ isSubmitting }) =>
isSubmitting ? (
<LoadingAnimation text="Validating API key" />
<div className="text-base">
<LoadingAnimation text="Validating API key" />
</div>
) : (
<Form>
<TextFormField
name="apiKey"
type="password"
label="OpenAI API Key:"
/>
<TextFormField name="apiKey" type="password" label="API Key:" />
<div className="flex">
<Button
size="xs"
type="submit"
disabled={isSubmitting}
className="w-64 mx-auto"
className="w-48 mx-auto"
>
Submit
</Button>

View File

@@ -3,46 +3,62 @@
import { useState, useEffect } from "react";
import { ApiKeyForm } from "./ApiKeyForm";
import { Modal } from "../Modal";
import { Text } from "@tremor/react";
import { Divider, Text } from "@tremor/react";
export async function checkApiKey() {
const response = await fetch("/api/manage/admin/genai-api-key/validate");
if (!response.ok && (response.status === 404 || response.status === 400)) {
const jsonResponse = await response.json();
return jsonResponse.detail;
}
return null;
}
export const ApiKeyModal = () => {
const [isOpen, setIsOpen] = useState(false);
const [errorMsg, setErrorMsg] = useState<string | null>(null);
useEffect(() => {
fetch("/api/manage/admin/genai-api-key/validate", {
method: "HEAD",
}).then((res) => {
// show popup if either the API key is not set or the API key is invalid
if (!res.ok && (res.status === 404 || res.status === 400)) {
setIsOpen(true);
checkApiKey().then((error) => {
console.log(error);
if (error) {
setErrorMsg(error);
}
});
}, []);
if (!isOpen) {
if (!errorMsg) {
return null;
}
return (
<Modal className="max-w-4xl" onOutsideClick={() => setIsOpen(false)}>
<Modal
title="LLM Key Setup"
className="max-w-4xl"
onOutsideClick={() => setErrorMsg(null)}
>
<div>
<div>
<Text className="mb-2.5">
Can&apos;t find a valid registered OpenAI API key. Please provide
one to be able to ask questions! Or if you&apos;d rather just look
around for now,{" "}
<div className="mb-2.5 text-base">
Please provide a valid OpenAI API key below in order to start using
Danswer Search or Danswer Chat.
<br />
<br />
Or if you&apos;d rather look around first,{" "}
<strong
onClick={() => setIsOpen(false)}
onClick={() => setErrorMsg(null)}
className="text-link cursor-pointer"
>
skip this step
</strong>
.
</Text>
</div>
<Divider />
<ApiKeyForm
handleResponse={(response) => {
if (response.ok) {
setIsOpen(false);
setErrorMsg(null);
}
}}
/>

View File

@@ -4,7 +4,7 @@ import { useRef, useState } from "react";
import { SearchBar } from "./SearchBar";
import { SearchResultsDisplay } from "./SearchResultsDisplay";
import { SourceSelector } from "./filtering/Filters";
import { Connector, DocumentSet, Tag } from "@/lib/types";
import { CCPairBasicInfo, Connector, DocumentSet, Tag } from "@/lib/types";
import {
DanswerDocument,
Quote,
@@ -36,7 +36,7 @@ const VALID_QUESTION_RESPONSE_DEFAULT: ValidQuestionResponse = {
};
interface SearchSectionProps {
connectors: Connector<any>[];
ccPairs: CCPairBasicInfo[];
documentSets: DocumentSet[];
personas: Persona[];
tags: Tag[];
@@ -44,7 +44,7 @@ interface SearchSectionProps {
}
export const SearchSection = ({
connectors,
ccPairs,
documentSets,
personas,
tags,
@@ -72,7 +72,7 @@ export const SearchSection = ({
// Filters
const filterManager = useFilters();
const availableSources = connectors.map((connector) => connector.source);
const availableSources = ccPairs.map((ccPair) => ccPair.source);
const [finalAvailableSources, finalAvailableDocumentSets] =
computeAvailableFilters({
selectedPersona: personas.find(
@@ -214,7 +214,7 @@ export const SearchSection = ({
return (
<div className="relative max-w-[2000px] xl:max-w-[1430px] mx-auto">
<div className="absolute left-0 hidden 2xl:block w-52 3xl:w-64">
{(connectors.length > 0 || documentSets.length > 0) && (
{(ccPairs.length > 0 || documentSets.length > 0) && (
<SourceSelector
{...filterManager}
availableDocumentSets={finalAvailableDocumentSets}

View File

@@ -191,6 +191,12 @@ export interface ConnectorIndexingStatus<
is_deletable: boolean;
}
export interface CCPairBasicInfo {
docs_indexed: number;
has_successful_run: boolean;
source: ValidSources;
}
// CREDENTIALS
export interface CredentialBase<T> {
credential_json: T;