Clearer onboarding + Provider Updates (#2361)

This commit is contained in:
pablodanswer 2024-09-08 13:35:20 -07:00 committed by GitHub
parent 148c2a7375
commit ace041415a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 514 additions and 289 deletions

View File

@ -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 }) => (
<Form className="gap-y-6 items-stretch mt-8">
<TextFormField
name="name"
label="Display Name"
subtext="A name which you can use to identify this provider when selecting it in the UI."
placeholder="Display Name"
disabled={existingLlmProvider ? true : false}
/>
<Form className="gap-y-4 items-stretch mt-6">
{!hideAdvanced && (
<TextFormField
name="name"
label="Display Name"
subtext="A name which you can use to identify this provider when selecting it in the UI."
placeholder="Display Name"
disabled={existingLlmProvider ? true : false}
/>
)}
{llmProviderDescriptor.api_key_required && (
<TextFormField
small={hideAdvanced}
name="api_key"
label="API Key"
placeholder="API Key"
@ -238,6 +243,7 @@ export function LLMProviderUpdateForm({
{llmProviderDescriptor.api_base_required && (
<TextFormField
small={hideAdvanced}
name="api_base"
label="API Base"
placeholder="API Base"
@ -246,6 +252,7 @@ export function LLMProviderUpdateForm({
{llmProviderDescriptor.api_version_required && (
<TextFormField
small={hideAdvanced}
name="api_version"
label="API Version"
placeholder="API Version"
@ -255,6 +262,7 @@ export function LLMProviderUpdateForm({
{llmProviderDescriptor.custom_config_keys?.map((customConfigKey) => (
<div key={customConfigKey.name}>
<TextFormField
small={hideAdvanced}
name={`custom_config.${customConfigKey.name}`}
label={
customConfigKey.is_required
@ -266,134 +274,144 @@ export function LLMProviderUpdateForm({
</div>
))}
<Divider />
{llmProviderDescriptor.llm_names.length > 0 ? (
<SelectorFormField
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
options={llmProviderDescriptor.llm_names.map((name) => ({
name: getDisplayNameForModel(name),
value: name,
}))}
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
placeholder="E.g. gpt-4"
/>
)}
{llmProviderDescriptor.llm_names.length > 0 ? (
<SelectorFormField
name="fast_default_model_name"
subtext={`The model to use for lighter flows like \`LLM Chunk Filter\`
for this provider. If \`Default\` is specified, will use
the Default Model configured above.`}
label="[Optional] Fast Model"
options={llmProviderDescriptor.llm_names.map((name) => ({
name: getDisplayNameForModel(name),
value: name,
}))}
includeDefault
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="fast_default_model_name"
subtext={`The model to use for lighter flows like \`LLM Chunk Filter\`
for this provider. If \`Default\` is specified, will use
the Default Model configured above.`}
label="[Optional] Fast Model"
placeholder="E.g. gpt-4"
/>
)}
<Divider />
{llmProviderDescriptor.name != "azure" && (
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
)}
{showAdvancedOptions && (
{!hideAdvanced && (
<>
{llmProviderDescriptor.llm_names.length > 0 && (
<div className="w-full">
<MultiSelectField
selectedInitially={values.display_model_names}
name="display_model_names"
label="Display Models"
subtext="Select the models to make available to users. Unselected models will not be available."
options={llmProviderDescriptor.llm_names.map((name) => ({
value: name,
label: getDisplayNameForModel(name),
}))}
onChange={(selected) =>
setFieldValue("display_model_names", selected)
}
/>
</div>
<Divider />
{llmProviderDescriptor.llm_names.length > 0 ? (
<SelectorFormField
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
options={llmProviderDescriptor.llm_names.map((name) => ({
name: getDisplayNameForModel(name),
value: name,
}))}
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="default_model_name"
subtext="The model to use by default for this provider unless otherwise specified."
label="Default Model"
placeholder="E.g. gpt-4"
/>
)}
{isPaidEnterpriseFeaturesEnabled && userGroups && (
<>
<BooleanFormField
small
removeIndent
alignTop
name="is_public"
label="Is Public?"
subtext="If set, this LLM Provider will be available to all users. If not, only the specified User Groups will be able to use it."
/>
{llmProviderDescriptor.llm_names.length > 0 ? (
<SelectorFormField
name="fast_default_model_name"
subtext={`The model to use for lighter flows like \`LLM Chunk Filter\`
for this provider. If \`Default\` is specified, will use
the Default Model configured above.`}
label="[Optional] Fast Model"
options={llmProviderDescriptor.llm_names.map((name) => ({
name: getDisplayNameForModel(name),
value: name,
}))}
includeDefault
maxHeight="max-h-56"
/>
) : (
<TextFormField
name="fast_default_model_name"
subtext={`The model to use for lighter flows like \`LLM Chunk Filter\`
for this provider. If \`Default\` is specified, will use
the Default Model configured above.`}
label="[Optional] Fast Model"
placeholder="E.g. gpt-4"
/>
)}
{userGroups && userGroups.length > 0 && !values.is_public && (
<div>
<Text>
Select which User Groups should have access to this LLM
Provider.
</Text>
<div className="flex flex-wrap gap-2 mt-2">
{userGroups.map((userGroup) => {
const isSelected = values.groups.includes(
userGroup.id
);
return (
<Bubble
key={userGroup.id}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
setFieldValue(
"groups",
values.groups.filter(
(id) => id !== userGroup.id
)
);
} else {
setFieldValue("groups", [
...values.groups,
userGroup.id,
]);
}
}}
>
<div className="flex">
<GroupsIcon />
<div className="ml-1">{userGroup.name}</div>
</div>
</Bubble>
);
})}
</div>
<Divider />
{llmProviderDescriptor.name != "azure" && (
<AdvancedOptionsToggle
showAdvancedOptions={showAdvancedOptions}
setShowAdvancedOptions={setShowAdvancedOptions}
/>
)}
{showAdvancedOptions && (
<>
{llmProviderDescriptor.llm_names.length > 0 && (
<div className="w-full">
<MultiSelectField
selectedInitially={values.display_model_names}
name="display_model_names"
label="Display Models"
subtext="Select the models to make available to users. Unselected models will not be available."
options={llmProviderDescriptor.llm_names.map(
(name) => ({
value: name,
label: getDisplayNameForModel(name),
})
)}
onChange={(selected) =>
setFieldValue("display_model_names", selected)
}
/>
</div>
)}
{isPaidEnterpriseFeaturesEnabled && userGroups && (
<>
<BooleanFormField
small
removeIndent
alignTop
name="is_public"
label="Is Public?"
subtext="If set, this LLM Provider will be available to all users. If not, only the specified User Groups will be able to use it."
/>
{userGroups &&
userGroups.length > 0 &&
!values.is_public && (
<div>
<Text>
Select which User Groups should have access to
this LLM Provider.
</Text>
<div className="flex flex-wrap gap-2 mt-2">
{userGroups.map((userGroup) => {
const isSelected = values.groups.includes(
userGroup.id
);
return (
<Bubble
key={userGroup.id}
isSelected={isSelected}
onClick={() => {
if (isSelected) {
setFieldValue(
"groups",
values.groups.filter(
(id) => id !== userGroup.id
)
);
} else {
setFieldValue("groups", [
...values.groups,
userGroup.id,
]);
}
}}
>
<div className="flex">
<GroupsIcon />
<div className="ml-1">
{userGroup.name}
</div>
</div>
</Bubble>
);
})}
</div>
</div>
)}
</>
)}
</>
)}
</>
@ -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();
}}

View File

@ -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<string | null>(null);
const explicitlyUntoggle = () => {
setShowDocSidebar(false);
@ -1588,6 +1598,11 @@ export function ChatPage({
return (
<>
<HealthCheckBanner />
{showApiKeyModal && !shouldShowWelcomeModal && (
<ApiKeyModal hide={() => 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()}
>
{/* <input {...getInputProps()} /> */}
<div
className={`w-full h-full flex flex-col overflow-y-auto include-scrollbar overflow-x-hidden relative`}
ref={scrollableDivRef}
@ -1770,7 +1784,8 @@ export function ChatPage({
{messageHistory.length === 0 &&
!isFetchingChatMessages &&
currentSessionChatState == "input" && (
currentSessionChatState == "input" &&
!loadingError && (
<ChatIntro
availableSources={finalAvailableSources}
selectedPersona={liveAssistant}
@ -2078,16 +2093,17 @@ export function ChatPage({
}
})}
{currentSessionChatState == "loading" &&
!currentSessionRegenerationState?.regenerating &&
messageHistory[messageHistory.length - 1]?.type !=
"user" && (
<HumanMessage
key={-2}
messageId={-1}
content={submittedMessage}
/>
)}
{currentSessionChatState == "loading" ||
(loadingError &&
!currentSessionRegenerationState?.regenerating &&
messageHistory[messageHistory.length - 1]
?.type != "user" && (
<HumanMessage
key={-2}
messageId={-1}
content={submittedMessage}
/>
))}
{currentSessionChatState == "loading" && (
<div
@ -2116,6 +2132,20 @@ export function ChatPage({
</div>
)}
{loadingError && (
<div key={-1}>
<AIMessage
currentPersona={liveAssistant}
messageId={-1}
personaName={liveAssistant.name}
content={
<p className="text-red-700 text-sm my-auto">
{loadingError}
</p>
}
/>
</div>
)}
{currentPersona &&
currentPersona.starter_messages &&
currentPersona.starter_messages.length > 0 &&
@ -2177,6 +2207,9 @@ export function ChatPage({
</div>
)}
<ChatInputBar
showConfigureAPIKey={() =>
setShowApiKeyModal(true)
}
chatState={currentSessionChatState}
stopGenerating={stopGenerating}
openModelSettings={() => setSettingsToggled(true)}

View File

@ -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 (
<FunctionalWrapper
initiallyToggled={initiallyToggled}
content={(toggledSidebar, toggle) => (
<ChatPage
toggle={toggle}
defaultSelectedAssistantId={defaultAssistantId}
toggledSidebar={toggledSidebar}
/>
<ChatPage toggle={toggle} toggledSidebar={toggledSidebar} />
)}
/>
);

View File

@ -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({
<div>
<SelectedFilterDisplay filterManager={filterManager} />
</div>
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
<div
className="
opacity-100

View File

@ -2,10 +2,10 @@ import { redirect } from "next/navigation";
import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { WelcomeModal } from "@/components/initialSetup/welcome/WelcomeModalWrapper";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
import { ChatProvider } from "@/components/context/ChatContext";
import { fetchChatData } from "@/lib/chat/fetchChatData";
import WrappedChat from "./WrappedChat";
import { ProviderContextProvider } from "@/components/chat_search/ProviderContext";
export default async function Page({
searchParams,
@ -23,7 +23,6 @@ export default async function Page({
const {
user,
chatSessions,
ccPairs,
availableSources,
documentSets,
assistants,
@ -33,9 +32,7 @@ export default async function Page({
toggleSidebar,
openedFolders,
defaultAssistantId,
finalDocumentSidebarInitialWidth,
shouldShowWelcomeModal,
shouldDisplaySourcesIncompleteModal,
userInputPrompts,
} = data;
@ -43,9 +40,7 @@ export default async function Page({
<>
<InstantSSRAutoRefresh />
{shouldShowWelcomeModal && <WelcomeModal user={user} />}
{!shouldShowWelcomeModal && !shouldDisplaySourcesIncompleteModal && (
<ApiKeyModal user={user} />
)}
<ChatProvider
value={{
chatSessions,
@ -57,12 +52,13 @@ export default async function Page({
folders,
openedFolders,
userInputPrompts,
shouldShowWelcomeModal,
defaultAssistantId,
}}
>
<WrappedChat
defaultAssistantId={defaultAssistantId}
initiallyToggled={toggleSidebar}
/>
<ProviderContextProvider>
<WrappedChat initiallyToggled={toggleSidebar} />
</ProviderContextProvider>
</ChatProvider>
</>
);

View File

@ -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({
<h1 className="text-2xl font-bold mb-4 text-error">Error</h1>
<p className="text-text-500">
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.
</p>
<p className="mt-4">
If you&apos;re an admin, please check{" "}
<a
className="text-link"
href="https://docs.danswer.dev/introduction?utm_source=app&utm_medium=error_page&utm_campaign=config_error"
target="_blank"
rel="noopener noreferrer"
>
our docs
</a>{" "}
to see how to configure Danswer properly. If you&apos;re a user,
please contact your admin to fix this error.
</p>
<p className="mt-4">
For additional support and guidance, you can reach out to our
community on{" "}
<a
className="text-link"
href="https://danswer.ai?utm_source=app&utm_medium=error_page&utm_campaign=config_error"
target="_blank"
rel="noopener noreferrer"
>
Slack
</a>
.
</p>
</Card>
</div>

View File

@ -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) => (
<SearchSection
disabledAgentic={disabledAgentic}
agenticSearchEnabled={agenticSearchEnabled}
toggle={toggle}
toggledSidebar={toggledSidebar}
querySessions={querySessions}
user={user}
ccPairs={ccPairs}
documentSets={documentSets}
personas={personas}
tags={tags}
defaultSearchType={searchTypeDefault}
/>
)}

View File

@ -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 && <WelcomeModal user={user} />}
<InstantSSRAutoRefresh />
{!shouldShowWelcomeModal &&
!shouldDisplayNoSourcesModal &&
!shouldDisplaySourcesIncompleteModal && <ApiKeyModal user={user} />}
{shouldDisplayNoSourcesModal && <NoSourcesModal />}
{shouldDisplaySourcesIncompleteModal && (
@ -199,18 +196,27 @@ export default async function Home() {
Only used in the EE version of the app. */}
<ChatPopup />
<WrappedSearch
disabledAgentic={DISABLE_LLM_DOC_RELEVANCE}
initiallyToggled={toggleSidebar}
querySessions={querySessions}
user={user}
ccPairs={ccPairs}
documentSets={documentSets}
personas={assistants}
tags={tags}
searchTypeDefault={searchTypeDefault}
agenticSearchEnabled={agenticSearchEnabled}
/>
<SearchProvider
value={{
querySessions,
ccPairs,
documentSets,
assistants,
tags,
agenticSearchEnabled,
disabledAgentic: DISABLE_LLM_DOC_RELEVANCE,
initiallyToggled: toggleSidebar,
shouldShowWelcomeModal,
shouldDisplayNoSources: shouldDisplayNoSourcesModal,
}}
>
<ProviderContextProvider>
<WrappedSearch
initiallyToggled={toggleSidebar}
searchTypeDefault={searchTypeDefault}
/>
</ProviderContextProvider>
</SearchProvider>
</>
);
}

View File

@ -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

View File

@ -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<void>; // Add this line
}
const ProviderContext = createContext<ProviderContextType | undefined>(
undefined
);
export function ProviderContextProvider({
children,
}: {
children: React.ReactNode;
}) {
const { user } = useUser();
const router = useRouter();
const [validProviderExists, setValidProviderExists] = useState<boolean>(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 (
<ProviderContext.Provider
value={{
shouldShowConfigurationNeeded,
providerOptions,
refreshProviderInfo, // Add this line
}}
>
{children}
</ProviderContext.Provider>
);
}
export function useProviderStatus() {
const context = useContext(ProviderContext);
if (context === undefined) {
throw new Error(
"useProviderStatus must be used within a ProviderContextProvider"
);
}
return context;
}

View File

@ -0,0 +1,27 @@
import { useProviderStatus } from "./ProviderContext";
export default function CredentialNotConfigured({
showConfigureAPIKey,
}: {
showConfigureAPIKey: () => void;
}) {
const { shouldShowConfigurationNeeded } = useProviderStatus();
if (!shouldShowConfigurationNeeded) {
return null;
}
return (
<p className="text-base text-center w-full text-subtle">
Please note that you have not yet configured an LLM provider. You can
configure one{" "}
<button
onClick={showConfigureAPIKey}
className="text-link hover:underline cursor-pointer"
>
here
</button>
.
</p>
);
}

View File

@ -1,4 +1,4 @@
import { Dispatch, SetStateAction, useEffect, useRef, useState } from "react";
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
interface UseSidebarVisibilityProps {
toggledSidebar: boolean;

View File

@ -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<string, boolean>;
userInputPrompts: InputPrompt[];
shouldShowWelcomeModal?: boolean;
shouldDisplaySourcesIncompleteModal?: boolean;
defaultAssistantId?: number;
refreshChatSessions: () => Promise<void>;
}
const ChatContext = createContext<ChatContextProps | undefined>(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<ChatContextProps, "refreshChatSessions">;
children: React.ReactNode;
}> = ({ value, children }) => {
return <ChatContext.Provider value={value}>{children}</ChatContext.Provider>;
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 (
<ChatContext.Provider
value={{ ...value, chatSessions, refreshChatSessions }}
>
{children}
</ChatContext.Provider>
);
};
export const useChatContext = (): ChatContextProps => {

View File

@ -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<SearchContextProps | undefined>(undefined);
export const SearchProvider: React.FC<{
value: SearchContextProps;
children: React.ReactNode;
}> = ({ value, children }) => {
return (
<SearchContext.Provider value={value}>{children}</SearchContext.Provider>
);
};
export const useSearchContext = (): SearchContextProps => {
const context = useContext(SearchContext);
if (!context) {
throw new Error("useSearchContext must be used within a SearchProvider");
}
return context;
};

View File

@ -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!
</Text>
}
icon={FiMessageSquare}
callToAction="Get Started"
onClick={() => {
setSelectedFlow("chat");

View File

@ -55,6 +55,7 @@ export const ApiKeyForm = ({
return (
<TabPanel key={provider.name}>
<LLMProviderUpdateForm
hideAdvanced
llmProviderDescriptor={provider}
onClose={() => onSuccess()}
shouldMarkAsDefault

View File

@ -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<boolean>(false);
const [validProviderExists, setValidProviderExists] = useState<boolean>(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 (
<Modal
title="LLM Key Setup"
className="max-w-4xl"
onOutsideClick={() => setForceHidden(true)}
title="Set an API Key!"
className="max-w-3xl"
onOutsideClick={() => hide()}
>
<div className="max-h-[75vh] overflow-y-auto flex flex-col px-4">
<div>
<div className="mb-5 text-sm">
Please setup an LLM below in order to start using Danswer Search or
Danswer Chat. Don&apos;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.
<br />
<br />
Or if you&apos;d rather look around first,{" "}
<strong
onClick={() => setForceHidden(true)}
className="text-link cursor-pointer"
>
If you&apos;d rather look around first, you can
<strong onClick={() => hide()} className="text-link cursor-pointer">
{" "}
skip this step
</strong>
.
@ -63,7 +41,8 @@ export const ApiKeyModal = ({ user }: { user: User | null }) => {
<ApiKeyForm
onSuccess={() => {
router.refresh();
setForceHidden(true);
refreshProviderInfo();
hide();
}}
providerOptions={providerOptions}
/>

View File

@ -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<string>("");
const [comments, setComments] = useState<any>(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<SearchType>(defaultSearchType);
const [selectedPersona, setSelectedPersona] = useState<number>(
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 = ({
<div className="flex relative pr-[8px] h-full text-default">
{popup}
{!shouldDisplayNoSources &&
showApiKeyModal &&
!shouldShowWelcomeModal && (
<ApiKeyModal hide={() => setShowApiKeyModal(false)} />
)}
{deletingChatSession && (
<DeleteEntityModal
entityType="search"
@ -747,6 +755,11 @@ export const SearchSection = ({
</div>
</div>
</div>
<UnconfiguredProviderText
showConfigureAPIKey={() => setShowApiKeyModal(true)}
/>
<FullSearchBar
toggleAgentic={
disabledAgentic ? undefined : toggleAgentic

View File

@ -44,7 +44,6 @@ interface FetchChatDataResult {
toggleSidebar: boolean;
finalDocumentSidebarInitialWidth?: number;
shouldShowWelcomeModal: boolean;
shouldDisplaySourcesIncompleteModal: boolean;
userInputPrompts: InputPrompt[];
}
@ -242,7 +241,6 @@ export async function fetchChatData(searchParams: {
finalDocumentSidebarInitialWidth,
toggleSidebar,
shouldShowWelcomeModal,
shouldDisplaySourcesIncompleteModal,
userInputPrompts,
};
}