From ace041415a35f6c3bebdeafa620a5f7c12ebdc5c Mon Sep 17 00:00:00 2001 From: pablodanswer Date: Sun, 8 Sep 2024 13:35:20 -0700 Subject: [PATCH] Clearer onboarding + Provider Updates (#2361) --- .../llm/LLMProviderUpdateForm.tsx | 301 ++++++++++-------- web/src/app/chat/ChatPage.tsx | 79 +++-- web/src/app/chat/WrappedChat.tsx | 8 +- web/src/app/chat/input/ChatInputBar.tsx | 8 + web/src/app/chat/page.tsx | 18 +- web/src/app/layout.tsx | 32 +- web/src/app/search/WrappedSearch.tsx | 27 -- web/src/app/search/page.tsx | 40 ++- web/src/components/admin/connectors/Field.tsx | 2 +- .../chat_search/ProviderContext.tsx | 70 ++++ .../chat_search/UnconfiguredProviderText.tsx | 27 ++ web/src/components/chat_search/hooks.ts | 2 +- web/src/components/context/ChatContext.tsx | 31 +- web/src/components/context/SearchContext.tsx | 38 +++ .../initialSetup/welcome/WelcomeModal.tsx | 3 - web/src/components/llm/ApiKeyForm.tsx | 1 + web/src/components/llm/ApiKeyModal.tsx | 57 ++-- web/src/components/search/SearchSection.tsx | 57 ++-- web/src/lib/chat/fetchChatData.ts | 2 - 19 files changed, 514 insertions(+), 289 deletions(-) create mode 100644 web/src/components/chat_search/ProviderContext.tsx create mode 100644 web/src/components/chat_search/UnconfiguredProviderText.tsx create mode 100644 web/src/components/context/SearchContext.tsx diff --git a/web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx b/web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx index 85cae14aa..677fa4bd9 100644 --- a/web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx +++ b/web/src/app/admin/configuration/llm/LLMProviderUpdateForm.tsx @@ -31,11 +31,13 @@ export function LLMProviderUpdateForm({ existingLlmProvider, shouldMarkAsDefault, setPopup, + hideAdvanced, }: { llmProviderDescriptor: WellKnownLLMProviderDescriptor; onClose: () => void; existingLlmProvider?: FullLLMProvider; shouldMarkAsDefault?: boolean; + hideAdvanced?: boolean; setPopup?: (popup: PopupSpec) => void; }) { const { mutate } = useSWRConfig(); @@ -52,7 +54,7 @@ export function LLMProviderUpdateForm({ // Define the initial values based on the provider's requirements const initialValues = { - name: existingLlmProvider?.name ?? "", + name: existingLlmProvider?.name || (hideAdvanced ? "Default" : ""), api_key: existingLlmProvider?.api_key ?? "", api_base: existingLlmProvider?.api_base ?? "", api_version: existingLlmProvider?.api_version ?? "", @@ -218,17 +220,20 @@ export function LLMProviderUpdateForm({ }} > {({ values, setFieldValue }) => ( -
- + + {!hideAdvanced && ( + + )} {llmProviderDescriptor.api_key_required && ( (
))} - - - {llmProviderDescriptor.llm_names.length > 0 ? ( - ({ - name: getDisplayNameForModel(name), - value: name, - }))} - maxHeight="max-h-56" - /> - ) : ( - - )} - - {llmProviderDescriptor.llm_names.length > 0 ? ( - ({ - name: getDisplayNameForModel(name), - value: name, - }))} - includeDefault - maxHeight="max-h-56" - /> - ) : ( - - )} - - - - {llmProviderDescriptor.name != "azure" && ( - - )} - - {showAdvancedOptions && ( + {!hideAdvanced && ( <> - {llmProviderDescriptor.llm_names.length > 0 && ( -
- ({ - value: name, - label: getDisplayNameForModel(name), - }))} - onChange={(selected) => - setFieldValue("display_model_names", selected) - } - /> -
+ + + {llmProviderDescriptor.llm_names.length > 0 ? ( + ({ + name: getDisplayNameForModel(name), + value: name, + }))} + maxHeight="max-h-56" + /> + ) : ( + )} - {isPaidEnterpriseFeaturesEnabled && userGroups && ( - <> - + {llmProviderDescriptor.llm_names.length > 0 ? ( + ({ + name: getDisplayNameForModel(name), + value: name, + }))} + includeDefault + maxHeight="max-h-56" + /> + ) : ( + + )} - {userGroups && userGroups.length > 0 && !values.is_public && ( -
- - Select which User Groups should have access to this LLM - Provider. - -
- {userGroups.map((userGroup) => { - const isSelected = values.groups.includes( - userGroup.id - ); - return ( - { - if (isSelected) { - setFieldValue( - "groups", - values.groups.filter( - (id) => id !== userGroup.id - ) - ); - } else { - setFieldValue("groups", [ - ...values.groups, - userGroup.id, - ]); - } - }} - > -
- -
{userGroup.name}
-
-
- ); - })} -
+ + + {llmProviderDescriptor.name != "azure" && ( + + )} + + {showAdvancedOptions && ( + <> + {llmProviderDescriptor.llm_names.length > 0 && ( +
+ ({ + value: name, + label: getDisplayNameForModel(name), + }) + )} + onChange={(selected) => + setFieldValue("display_model_names", selected) + } + />
)} + + {isPaidEnterpriseFeaturesEnabled && userGroups && ( + <> + + + {userGroups && + userGroups.length > 0 && + !values.is_public && ( +
+ + Select which User Groups should have access to + this LLM Provider. + +
+ {userGroups.map((userGroup) => { + const isSelected = values.groups.includes( + userGroup.id + ); + return ( + { + if (isSelected) { + setFieldValue( + "groups", + values.groups.filter( + (id) => id !== userGroup.id + ) + ); + } else { + setFieldValue("groups", [ + ...values.groups, + userGroup.id, + ]); + } + }} + > +
+ +
+ {userGroup.name} +
+
+
+ ); + })} +
+
+ )} + + )} )} @@ -432,6 +450,27 @@ export function LLMProviderUpdateForm({ return; } + // If the deleted provider was the default, set the first remaining provider as default + const remainingProvidersResponse = await fetch( + LLM_PROVIDERS_ADMIN_URL + ); + if (remainingProvidersResponse.ok) { + const remainingProviders = + await remainingProvidersResponse.json(); + + if (remainingProviders.length > 0) { + const setDefaultResponse = await fetch( + `${LLM_PROVIDERS_ADMIN_URL}/${remainingProviders[0].id}/default`, + { + method: "POST", + } + ); + if (!setDefaultResponse.ok) { + console.error("Failed to set new default provider"); + } + } + } + mutate(LLM_PROVIDERS_ADMIN_URL); onClose(); }} diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index 57da93a91..0a4d4ee24 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -98,6 +98,7 @@ import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal"; import { SEARCH_TOOL_NAME } from "./tools/constants"; import { useUser } from "@/components/user/UserProvider"; +import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; const TEMP_USER_MESSAGE_ID = -1; const TEMP_ASSISTANT_MESSAGE_ID = -2; @@ -106,12 +107,10 @@ const SYSTEM_MESSAGE_ID = -3; export function ChatPage({ toggle, documentSidebarInitialWidth, - defaultSelectedAssistantId, toggledSidebar, }: { toggle: (toggled?: boolean) => void; documentSidebarInitialWidth?: number; - defaultSelectedAssistantId?: number; toggledSidebar: boolean; }) { const router = useRouter(); @@ -126,8 +125,13 @@ export function ChatPage({ folders, openedFolders, userInputPrompts, + defaultAssistantId, + shouldShowWelcomeModal, + refreshChatSessions, } = useChatContext(); + const [showApiKeyModal, setShowApiKeyModal] = useState(true); + const { user, refreshUser, isLoadingUser } = useUser(); // chat session @@ -162,9 +166,9 @@ export function ChatPage({ ? availableAssistants.find( (assistant) => assistant.id === existingChatSessionAssistantId ) - : defaultSelectedAssistantId !== undefined + : defaultAssistantId !== undefined ? availableAssistants.find( - (assistant) => assistant.id === defaultSelectedAssistantId + (assistant) => assistant.id === defaultAssistantId ) : undefined ); @@ -327,8 +331,8 @@ export function ChatPage({ async function initialSessionFetch() { if (existingChatSessionId === null) { setIsFetchingChatMessages(false); - if (defaultSelectedAssistantId !== undefined) { - setSelectedAssistantFromId(defaultSelectedAssistantId); + if (defaultAssistantId !== undefined) { + setSelectedAssistantFromId(defaultAssistantId); } else { setSelectedAssistant(undefined); } @@ -402,7 +406,7 @@ export function ChatPage({ // force re-name if the chat session doesn't have one if (!chatSession.description) { await nameChatSession(existingChatSessionId, seededMessage); - router.refresh(); // need to refresh to update name on sidebar + refreshChatSessions(); } } } @@ -676,12 +680,10 @@ export function ChatPage({ useEffect(() => { if (messageHistory.length === 0 && chatSessionIdRef.current === null) { setSelectedAssistant( - filteredAssistants.find( - (persona) => persona.id === defaultSelectedAssistantId - ) + filteredAssistants.find((persona) => persona.id === defaultAssistantId) ); } - }, [defaultSelectedAssistantId]); + }, [defaultAssistantId]); const [ selectedDocuments, @@ -1111,6 +1113,12 @@ export function ChatPage({ console.error( "First packet should contain message response info " ); + if (Object.hasOwn(packet, "error")) { + const error = (packet as StreamingError).error; + setLoadingError(error); + updateChatState("input"); + return; + } continue; } @@ -1330,6 +1338,7 @@ export function ChatPage({ if (!searchParamBasedChatSessionName) { await new Promise((resolve) => setTimeout(resolve, 200)); await nameChatSession(currChatSessionId, currMessage); + refreshChatSessions(); } // NOTE: don't switch pages if the user has navigated away from the chat @@ -1465,6 +1474,7 @@ export function ChatPage({ // Used to maintain a "time out" for history sidebar so our existing refs can have time to process change const [untoggled, setUntoggled] = useState(false); + const [loadingError, setLoadingError] = useState(null); const explicitlyUntoggle = () => { setShowDocSidebar(false); @@ -1588,6 +1598,11 @@ export function ChatPage({ return ( <> + + {showApiKeyModal && !shouldShowWelcomeModal && ( + setShowApiKeyModal(false)} /> + )} + {/* ChatPopup is a custom popup that displays a admin-specified message on initial user visit. Only used in the EE version of the app. */} {popup} @@ -1760,7 +1775,6 @@ export function ChatPage({ className={`h-full w-full relative flex-auto transition-margin duration-300 overflow-x-auto mobile:pb-12 desktop:pb-[100px]`} {...getRootProps()} > - {/* */}
- )} + {currentSessionChatState == "loading" || + (loadingError && + !currentSessionRegenerationState?.regenerating && + messageHistory[messageHistory.length - 1] + ?.type != "user" && ( + + ))} {currentSessionChatState == "loading" && (
)} + {loadingError && ( +
+ + {loadingError} +

+ } + /> +
+ )} {currentPersona && currentPersona.starter_messages && currentPersona.starter_messages.length > 0 && @@ -2177,6 +2207,9 @@ export function ChatPage({
)} + setShowApiKeyModal(true) + } chatState={currentSessionChatState} stopGenerating={stopGenerating} openModelSettings={() => setSettingsToggled(true)} diff --git a/web/src/app/chat/WrappedChat.tsx b/web/src/app/chat/WrappedChat.tsx index cdb8508df..6b48e4421 100644 --- a/web/src/app/chat/WrappedChat.tsx +++ b/web/src/app/chat/WrappedChat.tsx @@ -3,21 +3,15 @@ import { ChatPage } from "./ChatPage"; import FunctionalWrapper from "./shared_chat_search/FunctionalWrapper"; export default function WrappedChat({ - defaultAssistantId, initiallyToggled, }: { - defaultAssistantId?: number; initiallyToggled: boolean; }) { return ( ( - + )} /> ); diff --git a/web/src/app/chat/input/ChatInputBar.tsx b/web/src/app/chat/input/ChatInputBar.tsx index 66fe490c0..1a5ecb046 100644 --- a/web/src/app/chat/input/ChatInputBar.tsx +++ b/web/src/app/chat/input/ChatInputBar.tsx @@ -33,12 +33,15 @@ import { Tooltip } from "@/components/tooltip/Tooltip"; import { Hoverable } from "@/components/Hoverable"; import { SettingsContext } from "@/components/settings/SettingsProvider"; import { ChatState } from "../types"; +import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText"; +import { useSearchContext } from "@/components/context/SearchContext"; const MAX_INPUT_HEIGHT = 200; export function ChatInputBar({ openModelSettings, showDocs, + showConfigureAPIKey, selectedDocuments, message, setMessage, @@ -62,6 +65,7 @@ export function ChatInputBar({ chatSessionId, inputPrompts, }: { + showConfigureAPIKey: () => void; openModelSettings: () => void; chatState: ChatState; stopGenerating: () => void; @@ -111,6 +115,7 @@ export function ChatInputBar({ } } }; + const settings = useContext(SettingsContext); const { llmProviders } = useChatContext(); @@ -364,6 +369,9 @@ export function ChatInputBar({
+ + +
{shouldShowWelcomeModal && } - {!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && ( - - )} + - + + + ); diff --git a/web/src/app/layout.tsx b/web/src/app/layout.tsx index 4bf5c7722..5b9435cbc 100644 --- a/web/src/app/layout.tsx +++ b/web/src/app/layout.tsx @@ -6,6 +6,7 @@ import { } from "@/components/settings/lib"; import { CUSTOM_ANALYTICS_ENABLED, + EE_ENABLED, SERVER_SIDE_ONLY__PAID_ENTERPRISE_FEATURES_ENABLED, } from "@/lib/constants"; import { SettingsProvider } from "@/components/settings/SettingsProvider"; @@ -53,6 +54,7 @@ export default async function RootLayout({ children: React.ReactNode; }) { const combinedSettings = await fetchSettingsSS(); + if (!combinedSettings) { // Just display a simple full page error if fetching fails. @@ -72,8 +74,34 @@ export default async function RootLayout({

Error

Your Danswer instance was not configured properly and your - settings could not be loaded. Please contact your admin to fix - this error. + settings could not be loaded. This could be due to an admin + configuration issue or an incomplete setup. +

+

+ If you're an admin, please check{" "} + + our docs + {" "} + to see how to configure Danswer properly. If you're a user, + please contact your admin to fix this error. +

+

+ For additional support and guidance, you can reach out to our + community on{" "} + + Slack + + .

diff --git a/web/src/app/search/WrappedSearch.tsx b/web/src/app/search/WrappedSearch.tsx index a420f5747..91dad5d38 100644 --- a/web/src/app/search/WrappedSearch.tsx +++ b/web/src/app/search/WrappedSearch.tsx @@ -1,31 +1,12 @@ "use client"; import { SearchSection } from "@/components/search/SearchSection"; import FunctionalWrapper from "../chat/shared_chat_search/FunctionalWrapper"; -import { CCPairBasicInfo, DocumentSet, Tag, User } from "@/lib/types"; -import { Persona } from "../admin/assistants/interfaces"; -import { ChatSession } from "../chat/interfaces"; export default function WrappedSearch({ - querySessions, - ccPairs, - documentSets, - personas, searchTypeDefault, - tags, - user, - agenticSearchEnabled, initiallyToggled, - disabledAgentic, }: { - disabledAgentic: boolean; - querySessions: ChatSession[]; - ccPairs: CCPairBasicInfo[]; - documentSets: DocumentSet[]; - personas: Persona[]; searchTypeDefault: string; - tags: Tag[]; - user: User | null; - agenticSearchEnabled: boolean; initiallyToggled: boolean; }) { return ( @@ -33,16 +14,8 @@ export default function WrappedSearch({ initiallyToggled={initiallyToggled} content={(toggledSidebar, toggle) => ( )} diff --git a/web/src/app/search/page.tsx b/web/src/app/search/page.tsx index 40b4c5e53..e317d271b 100644 --- a/web/src/app/search/page.tsx +++ b/web/src/app/search/page.tsx @@ -5,7 +5,6 @@ import { } from "@/lib/userSS"; import { redirect } from "next/navigation"; import { HealthCheckBanner } from "@/components/health/healthcheck"; -import { ApiKeyModal } from "@/components/llm/ApiKeyModal"; import { fetchSS } from "@/lib/utilsSS"; import { CCPairBasicInfo, DocumentSet, Tag, User } from "@/lib/types"; import { cookies } from "next/headers"; @@ -34,6 +33,8 @@ import { DISABLE_LLM_DOC_RELEVANCE, } from "@/lib/constants"; import WrappedSearch from "./WrappedSearch"; +import { SearchProvider } from "@/components/context/SearchContext"; +import { ProviderContextProvider } from "@/components/chat_search/ProviderContext"; export default async function Home() { // Disable caching so we always get the up to date connector / document set / persona info @@ -185,10 +186,6 @@ export default async function Home() { {shouldShowWelcomeModal && } - {!shouldShowWelcomeModal && - !shouldDisplayNoSourcesModal && - !shouldDisplaySourcesIncompleteModal && } - {shouldDisplayNoSourcesModal && } {shouldDisplaySourcesIncompleteModal && ( @@ -199,18 +196,27 @@ export default async function Home() { Only used in the EE version of the app. */} - + + + + + ); } diff --git a/web/src/components/admin/connectors/Field.tsx b/web/src/components/admin/connectors/Field.tsx index 361a1916f..cbbc7b6d5 100644 --- a/web/src/components/admin/connectors/Field.tsx +++ b/web/src/components/admin/connectors/Field.tsx @@ -198,7 +198,7 @@ export function TextFormField({ rounded-lg w-full py-2 - px-3 + px-3 mt-1 placeholder:font-description placeholder:text-base diff --git a/web/src/components/chat_search/ProviderContext.tsx b/web/src/components/chat_search/ProviderContext.tsx new file mode 100644 index 000000000..3907b98a6 --- /dev/null +++ b/web/src/components/chat_search/ProviderContext.tsx @@ -0,0 +1,70 @@ +"use client"; +import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; +import React, { createContext, useContext, useState, useEffect } from "react"; +import { useUser } from "../user/UserProvider"; +import { useRouter } from "next/navigation"; +import { checkLlmProvider } from "../initialSetup/welcome/lib"; + +interface ProviderContextType { + shouldShowConfigurationNeeded: boolean; + providerOptions: WellKnownLLMProviderDescriptor[]; + refreshProviderInfo: () => Promise; // Add this line +} + +const ProviderContext = createContext( + undefined +); + +export function ProviderContextProvider({ + children, +}: { + children: React.ReactNode; +}) { + const { user } = useUser(); + const router = useRouter(); + + const [validProviderExists, setValidProviderExists] = useState(true); + const [providerOptions, setProviderOptions] = useState< + WellKnownLLMProviderDescriptor[] + >([]); + + const fetchProviderInfo = async () => { + const { providers, options, defaultCheckSuccessful } = + await checkLlmProvider(user); + setValidProviderExists(providers.length > 0 && defaultCheckSuccessful); + setProviderOptions(options); + }; + + useEffect(() => { + fetchProviderInfo(); + }, [router, user]); + + const shouldShowConfigurationNeeded = + !validProviderExists && providerOptions.length > 0; + + const refreshProviderInfo = async () => { + await fetchProviderInfo(); + }; + + return ( + + {children} + + ); +} + +export function useProviderStatus() { + const context = useContext(ProviderContext); + if (context === undefined) { + throw new Error( + "useProviderStatus must be used within a ProviderContextProvider" + ); + } + return context; +} diff --git a/web/src/components/chat_search/UnconfiguredProviderText.tsx b/web/src/components/chat_search/UnconfiguredProviderText.tsx new file mode 100644 index 000000000..e990eecd6 --- /dev/null +++ b/web/src/components/chat_search/UnconfiguredProviderText.tsx @@ -0,0 +1,27 @@ +import { useProviderStatus } from "./ProviderContext"; + +export default function CredentialNotConfigured({ + showConfigureAPIKey, +}: { + showConfigureAPIKey: () => void; +}) { + const { shouldShowConfigurationNeeded } = useProviderStatus(); + + if (!shouldShowConfigurationNeeded) { + return null; + } + + return ( +

+ Please note that you have not yet configured an LLM provider. You can + configure one{" "} + + . +

+ ); +} diff --git a/web/src/components/chat_search/hooks.ts b/web/src/components/chat_search/hooks.ts index 3377d9172..314810b55 100644 --- a/web/src/components/chat_search/hooks.ts +++ b/web/src/components/chat_search/hooks.ts @@ -1,4 +1,4 @@ -import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react"; +import { Dispatch, SetStateAction, useEffect, useRef } from "react"; interface UseSidebarVisibilityProps { toggledSidebar: boolean; diff --git a/web/src/components/context/ChatContext.tsx b/web/src/components/context/ChatContext.tsx index 9ab0c1e2e..06a63c903 100644 --- a/web/src/components/context/ChatContext.tsx +++ b/web/src/components/context/ChatContext.tsx @@ -1,6 +1,6 @@ "use client"; -import React, { createContext, useContext } from "react"; +import React, { createContext, useContext, useState } from "react"; import { DocumentSet, Tag, User, ValidSources } from "@/lib/types"; import { ChatSession } from "@/app/chat/interfaces"; import { Persona } from "@/app/admin/assistants/interfaces"; @@ -18,15 +18,40 @@ interface ChatContextProps { folders: Folder[]; openedFolders: Record; userInputPrompts: InputPrompt[]; + shouldShowWelcomeModal?: boolean; + shouldDisplaySourcesIncompleteModal?: boolean; + defaultAssistantId?: number; + refreshChatSessions: () => Promise; } const ChatContext = createContext(undefined); +// We use Omit to exclude 'refreshChatSessions' from the value prop type +// because we're defining it within the component export const ChatProvider: React.FC<{ - value: ChatContextProps; + value: Omit; children: React.ReactNode; }> = ({ value, children }) => { - return {children}; + const [chatSessions, setChatSessions] = useState(value?.chatSessions || []); + + const refreshChatSessions = async () => { + try { + const response = await fetch("/api/chat/get-user-chat-sessions"); + if (!response.ok) throw new Error("Failed to fetch chat sessions"); + const { sessions } = await response.json(); + setChatSessions(sessions); + } catch (error) { + console.error("Error refreshing chat sessions:", error); + } + }; + + return ( + + {children} + + ); }; export const useChatContext = (): ChatContextProps => { diff --git a/web/src/components/context/SearchContext.tsx b/web/src/components/context/SearchContext.tsx new file mode 100644 index 000000000..a46fbed24 --- /dev/null +++ b/web/src/components/context/SearchContext.tsx @@ -0,0 +1,38 @@ +"use client"; + +import React, { createContext, useContext } from "react"; +import { CCPairBasicInfo, DocumentSet, Tag } from "@/lib/types"; +import { Persona } from "@/app/admin/assistants/interfaces"; +import { ChatSession } from "@/app/chat/interfaces"; + +interface SearchContextProps { + querySessions: ChatSession[]; + ccPairs: CCPairBasicInfo[]; + documentSets: DocumentSet[]; + assistants: Persona[]; + tags: Tag[]; + agenticSearchEnabled: boolean; + disabledAgentic: boolean; + initiallyToggled: boolean; + shouldShowWelcomeModal: boolean; + shouldDisplayNoSources: boolean; +} + +const SearchContext = createContext(undefined); + +export const SearchProvider: React.FC<{ + value: SearchContextProps; + children: React.ReactNode; +}> = ({ value, children }) => { + return ( + {children} + ); +}; + +export const useSearchContext = (): SearchContextProps => { + const context = useContext(SearchContext); + if (!context) { + throw new Error("useSearchContext must be used within a SearchProvider"); + } + return context; +}; diff --git a/web/src/components/initialSetup/welcome/WelcomeModal.tsx b/web/src/components/initialSetup/welcome/WelcomeModal.tsx index c9472992f..ec71c5e6d 100644 --- a/web/src/components/initialSetup/welcome/WelcomeModal.tsx +++ b/web/src/components/initialSetup/welcome/WelcomeModal.tsx @@ -27,13 +27,11 @@ function UsageTypeSection({ title, description, callToAction, - icon, onClick, }: { title: string; description: string | JSX.Element; callToAction: string; - icon?: React.ElementType; onClick: () => void; }) { return ( @@ -243,7 +241,6 @@ export function _WelcomeModal({ user }: { user: User | null }) { this is the option for you! } - icon={FiMessageSquare} callToAction="Get Started" onClick={() => { setSelectedFlow("chat"); diff --git a/web/src/components/llm/ApiKeyForm.tsx b/web/src/components/llm/ApiKeyForm.tsx index 0ebe38dc3..1a1f24d91 100644 --- a/web/src/components/llm/ApiKeyForm.tsx +++ b/web/src/components/llm/ApiKeyForm.tsx @@ -55,6 +55,7 @@ export const ApiKeyForm = ({ return ( onSuccess()} shouldMarkAsDefault diff --git a/web/src/components/llm/ApiKeyModal.tsx b/web/src/components/llm/ApiKeyModal.tsx index 8c522fdc3..706413b4e 100644 --- a/web/src/components/llm/ApiKeyModal.tsx +++ b/web/src/components/llm/ApiKeyModal.tsx @@ -1,60 +1,38 @@ "use client"; -import { useState, useEffect } from "react"; import { ApiKeyForm } from "./ApiKeyForm"; import { Modal } from "../Modal"; -import { WellKnownLLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"; -import { checkLlmProvider } from "../initialSetup/welcome/lib"; -import { User } from "@/lib/types"; import { useRouter } from "next/navigation"; +import { useProviderStatus } from "../chat_search/ProviderContext"; -export const ApiKeyModal = ({ user }: { user: User | null }) => { +export const ApiKeyModal = ({ hide }: { hide: () => void }) => { const router = useRouter(); - const [forceHidden, setForceHidden] = useState(false); - const [validProviderExists, setValidProviderExists] = useState(true); - const [providerOptions, setProviderOptions] = useState< - WellKnownLLMProviderDescriptor[] - >([]); + const { + shouldShowConfigurationNeeded, + providerOptions, + refreshProviderInfo, + } = useProviderStatus(); - useEffect(() => { - async function fetchProviderInfo() { - const { providers, options, defaultCheckSuccessful } = - await checkLlmProvider(user); - setValidProviderExists(providers.length > 0 && defaultCheckSuccessful); - setProviderOptions(options); - } - - fetchProviderInfo(); - }, []); - - // don't show if - // (1) a valid provider has been setup or - // (2) there are no provider options (e.g. user isn't an admin) - // (3) user explicitly hides the modal - if (validProviderExists || !providerOptions.length || forceHidden) { + if (!shouldShowConfigurationNeeded) { return null; } return ( setForceHidden(true)} + title="Set an API Key!" + className="max-w-3xl" + onOutsideClick={() => hide()} >
- Please setup an LLM below in order to start using Danswer Search or - Danswer Chat. Don't worry, you can always change this later in - the Admin Panel. + Please provide an API Key below in order to start using + Danswer – you can always change this later.
-
- Or if you'd rather look around first,{" "} - setForceHidden(true)} - className="text-link cursor-pointer" - > + If you'd rather look around first, you can + hide()} className="text-link cursor-pointer"> + {" "} skip this step . @@ -63,7 +41,8 @@ export const ApiKeyModal = ({ user }: { user: User | null }) => { { router.refresh(); - setForceHidden(true); + refreshProviderInfo(); + hide(); }} providerOptions={providerOptions} /> diff --git a/web/src/components/search/SearchSection.tsx b/web/src/components/search/SearchSection.tsx index 1bf000218..cb33d9618 100644 --- a/web/src/components/search/SearchSection.tsx +++ b/web/src/components/search/SearchSection.tsx @@ -37,6 +37,10 @@ import { FeedbackModal } from "@/app/chat/modal/FeedbackModal"; import { deleteChatSession, handleChatFeedback } from "@/app/chat/lib"; import SearchAnswer from "./SearchAnswer"; import { DeleteEntityModal } from "../modals/DeleteEntityModal"; +import { ApiKeyModal } from "../llm/ApiKeyModal"; +import { useSearchContext } from "../context/SearchContext"; +import { useUser } from "../user/UserProvider"; +import UnconfiguredProviderText from "../chat_search/UnconfiguredProviderText"; export type searchState = | "input" @@ -58,33 +62,28 @@ const VALID_QUESTION_RESPONSE_DEFAULT: ValidQuestionResponse = { }; interface SearchSectionProps { - disabledAgentic: boolean; - ccPairs: CCPairBasicInfo[]; - documentSets: DocumentSet[]; - personas: Persona[]; - tags: Tag[]; toggle: () => void; - querySessions: ChatSession[]; defaultSearchType: SearchType; - user: User | null; toggledSidebar: boolean; - agenticSearchEnabled: boolean; } export const SearchSection = ({ - ccPairs, toggle, - disabledAgentic, - documentSets, - agenticSearchEnabled, - personas, - user, - tags, - querySessions, toggledSidebar, defaultSearchType, }: SearchSectionProps) => { - // Search Bar + const { + querySessions, + ccPairs, + documentSets, + assistants, + tags, + shouldShowWelcomeModal, + agenticSearchEnabled, + disabledAgentic, + shouldDisplayNoSources, + } = useSearchContext(); + const [query, setQuery] = useState(""); const [comments, setComments] = useState(null); const [contentEnriched, setContentEnriched] = useState(false); @@ -100,6 +99,8 @@ export const SearchSection = ({ messageId: null, }); + const [showApiKeyModal, setShowApiKeyModal] = useState(true); + const [agentic, setAgentic] = useState(agenticSearchEnabled); const toggleAgentic = () => { @@ -147,7 +148,7 @@ export const SearchSection = ({ useState(defaultSearchType); const [selectedPersona, setSelectedPersona] = useState( - personas[0]?.id || 0 + assistants[0]?.id || 0 ); // Used for search state display @@ -158,8 +159,8 @@ export const SearchSection = ({ const availableSources = ccPairs.map((ccPair) => ccPair.source); const [finalAvailableSources, finalAvailableDocumentSets] = computeAvailableFilters({ - selectedPersona: personas.find( - (persona) => persona.id === selectedPersona + selectedPersona: assistants.find( + (assistant) => assistant.id === selectedPersona ), availableSources: availableSources, availableDocumentSets: documentSets, @@ -362,6 +363,7 @@ export const SearchSection = ({ setSearchState("input"); } }; + const { user } = useUser(); const [searchAnswerExpanded, setSearchAnswerExpanded] = useState(false); const resetInput = (finalized?: boolean) => { @@ -403,8 +405,8 @@ export const SearchSection = ({ documentSets: filterManager.selectedDocumentSets, timeRange: filterManager.timeRange, tags: filterManager.selectedTags, - persona: personas.find( - (persona) => persona.id === selectedPersona + persona: assistants.find( + (assistant) => assistant.id === selectedPersona ) as Persona, updateCurrentAnswer: cancellable({ cancellationToken: lastSearchCancellationToken.current, @@ -595,6 +597,12 @@ export const SearchSection = ({
{popup} + {!shouldDisplayNoSources && + showApiKeyModal && + !shouldShowWelcomeModal && ( + setShowApiKeyModal(false)} /> + )} + {deletingChatSession && (
+ + setShowApiKeyModal(true)} + /> +