UI for model selection

This commit is contained in:
Weves
2024-01-26 21:16:41 -08:00
committed by Chris Weaver
parent 4b45164496
commit 2138c0b69d
19 changed files with 781 additions and 50 deletions

View File

@@ -2,7 +2,12 @@
import { ThreeDotsLoader } from "@/components/Loading";
import { PageSelector } from "@/components/PageSelector";
import { CPUIcon, EditIcon, TrashIcon } from "@/components/icons/icons";
import {
CPUIcon,
EditIcon,
SlackIcon,
TrashIcon,
} from "@/components/icons/icons";
import { SlackBotConfig } from "@/lib/types";
import { useState } from "react";
import { useSlackBotConfigs, useSlackBotTokens } from "./hooks";
@@ -21,7 +26,12 @@ import {
Text,
Title,
} from "@tremor/react";
import { FiArrowUpRight, FiChevronDown, FiChevronUp } from "react-icons/fi";
import {
FiArrowUpRight,
FiChevronDown,
FiChevronUp,
FiSlack,
} from "react-icons/fi";
import Link from "next/link";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
@@ -283,7 +293,7 @@ const Page = () => {
return (
<div className="container mx-auto">
<AdminPageTitle
icon={<CPUIcon size={32} />}
icon={<FiSlack size={32} />}
title="Slack Bot Configuration"
/>
<InstantSSRAutoRefresh />

View File

@@ -31,7 +31,8 @@ function Main({ ccPairId }: { ccPairId: number }) {
error,
} = useSWR<CCPairFullInfo>(
buildCCPairInfoUrl(ccPairId),
errorHandlingFetcher
errorHandlingFetcher,
{ refreshInterval: 5000 } // 5 seconds
);
if (isLoading) {

View File

@@ -18,7 +18,7 @@ interface Props {
export const ConnectorEditPopup = ({ existingConnector, onSubmit }: Props) => {
return (
<Modal onOutsideClick={onSubmit}>
<div className="px-8 py-6 bg-background">
<div className="bg-background">
<h2 className="text-xl font-bold flex">
Update Google Drive Connector
<div

View File

@@ -17,7 +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";
import { FiEdit } from "react-icons/fi";
const NUM_IN_PAGE = 20;

View File

@@ -7,6 +7,7 @@ import { ApiKeyForm } from "@/components/openai/ApiKeyForm";
import { GEN_AI_API_KEY_URL } from "@/components/openai/constants";
import { fetcher } from "@/lib/fetcher";
import { Text, Title } from "@tremor/react";
import { FiCpu } from "react-icons/fi";
import useSWR, { mutate } from "swr";
const ExistingKeys = () => {
@@ -51,7 +52,10 @@ const ExistingKeys = () => {
const Page = () => {
return (
<div className="mx-auto container">
<AdminPageTitle title="OpenAI Keys" icon={<KeyIcon size={32} />} />
<AdminPageTitle
title="LLM Keys"
icon={<FiCpu size={32} className="my-auto" />}
/>
<ExistingKeys />

View File

@@ -0,0 +1,56 @@
import { Modal } from "@/components/Modal";
import { Button, Text } from "@tremor/react";
export function ModelSelectionConfirmaion({
selectedModel,
onConfirm,
}: {
selectedModel: string;
onConfirm: () => void;
}) {
return (
<div className="mb-4">
<Text className="text-lg mb-4">
You have selected: <b>{selectedModel}</b>. Are you sure you want to
update to this new embedding model?
</Text>
<Text className="text-lg mb-2">
We will re-index all your documents in the background so you will be
able to continue to use Danswer as normal with the old model in the
meantime. Depending on how many documents you have indexed, this may
take a while.
</Text>
<Text className="text-lg mb-2">
<i>NOTE:</i> this re-indexing process will consume more resources than
normal. If you are self-hosting, we recommend that you allocate at least
16GB of RAM to Danswer during this process.
</Text>
<div className="flex mt-8">
<Button className="mx-auto" color="green" onClick={onConfirm}>
Confirm
</Button>
</div>
</div>
);
}
export function ModelSelectionConfirmaionModal({
selectedModel,
onConfirm,
onCancel,
}: {
selectedModel: string;
onConfirm: () => void;
onCancel: () => void;
}) {
return (
<Modal title="Update Embedding Model" onOutsideClick={onCancel}>
<div>
<ModelSelectionConfirmaion
selectedModel={selectedModel}
onConfirm={onConfirm}
/>
</div>
</Modal>
);
}

View File

@@ -0,0 +1,77 @@
import { DefaultDropdown, StringOrNumberOption } from "@/components/Dropdown";
import { Title, Text } from "@tremor/react";
import { FullEmbeddingModelDescriptor } from "./embeddingModels";
import { FiStar } from "react-icons/fi";
export function ModelOption({
model,
onSelect,
}: {
model: FullEmbeddingModelDescriptor;
onSelect?: (modelName: string) => void;
}) {
return (
<div
className={
"p-2 border border-border rounded shadow-md bg-hover-light w-96 flex flex-col"
}
>
<div className="font-bold text-lg flex">
{model.isDefault && <FiStar className="my-auto mr-1 text-accent" />}
{model.model_name}
</div>
<div className="text-sm mt-1 mx-1">{model.description}</div>
{model.link && (
<a
target="_blank"
href={model.link}
className="text-xs text-link mx-1 mt-1"
>
See More Details
</a>
)}
{onSelect && (
<div
className={`
m-auto
flex
mt-3
mb-1
w-fit
p-2
rounded-lg
bg-background
border
border-border
cursor-pointer
hover:bg-hover
text-sm
mt-auto`}
onClick={() => onSelect(model.model_name)}
>
Select Model
</div>
)}
</div>
);
}
export function ModelSelector({
modelOptions,
setSelectedModel,
}: {
modelOptions: FullEmbeddingModelDescriptor[];
setSelectedModel: (modelName: string) => void;
}) {
return (
<div className="flex flex-wrap gap-4">
{modelOptions.map((modelOption) => (
<ModelOption
key={modelOption.model_name}
model={modelOption}
onSelect={setSelectedModel}
/>
))}
</div>
);
}

View File

@@ -0,0 +1,78 @@
import { PageSelector } from "@/components/PageSelector";
import { CCPairStatus, IndexAttemptStatus } from "@/components/Status";
import { ConnectorIndexingStatus, ValidStatuses } from "@/lib/types";
import {
Button,
Table,
TableBody,
TableCell,
TableHead,
TableHeaderCell,
TableRow,
} from "@tremor/react";
import Link from "next/link";
import { useState } from "react";
import { FiMaximize2 } from "react-icons/fi";
export function ReindexingProgressTable({
reindexingProgress,
}: {
reindexingProgress: ConnectorIndexingStatus<any, any>[];
}) {
const numToDisplay = 10;
const [page, setPage] = useState(1);
return (
<div>
<Table>
<TableHead>
<TableRow>
<TableHeaderCell>Connector Name</TableHeaderCell>
<TableHeaderCell>Status</TableHeaderCell>
<TableHeaderCell>Docs Re-Indexed</TableHeaderCell>
</TableRow>
</TableHead>
<TableBody>
{reindexingProgress
.slice(numToDisplay * (page - 1), numToDisplay * page)
.map((reindexingProgress) => {
return (
<TableRow key={reindexingProgress.name}>
<TableCell>
<Link
href={`/admin/connector/${reindexingProgress.cc_pair_id}`}
className="text-link cursor-pointer flex"
>
<FiMaximize2 className="my-auto mr-1" />
{reindexingProgress.name}
</Link>
</TableCell>
<TableCell>
{reindexingProgress.latest_index_attempt?.status && (
<IndexAttemptStatus
status={reindexingProgress.latest_index_attempt.status}
/>
)}
</TableCell>
<TableCell>
{reindexingProgress?.latest_index_attempt
?.total_docs_indexed || "-"}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
<div className="mt-3 flex">
<div className="mx-auto">
<PageSelector
totalPages={Math.ceil(reindexingProgress.length / numToDisplay)}
currentPage={page}
onPageChange={(newPage) => setPage(newPage)}
/>
</div>
</div>
</div>
);
}

View File

@@ -0,0 +1,73 @@
export interface EmbeddingModelResponse {
model_name: string | null;
}
export interface EmbeddingModelDescriptor {
model_name: string;
model_dim: number;
normalize: boolean;
query_prefix?: string;
passage_prefix?: string;
}
export interface FullEmbeddingModelDescriptor extends EmbeddingModelDescriptor {
description: string;
isDefault?: boolean;
link?: string;
}
export const AVAILABLE_MODELS: FullEmbeddingModelDescriptor[] = [
{
model_name: "intfloat/e5-base-v2",
model_dim: 768,
normalize: true,
description:
"The recommended default for most situations. If you aren't sure which model to use, this is probably the one.",
isDefault: true,
link: "https://huggingface.co/intfloat/e5-base-v2",
query_prefix: "query: ",
passage_prefix: "passage: ",
},
{
model_name: "intfloat/e5-small-v2",
model_dim: 384,
normalize: true,
description:
"A smaller / faster version of the default model. If you're running Danswer on a resource constrained system, then this is a good choice.",
link: "https://huggingface.co/intfloat/e5-small-v2",
query_prefix: "query: ",
passage_prefix: "passage: ",
},
{
model_name: "intfloat/multilingual-e5-base",
model_dim: 768,
normalize: true,
description:
"If you have many documents in other languages besides English, this is the one to go for.",
link: "https://huggingface.co/intfloat/multilingual-e5-base",
query_prefix: "query: ",
passage_prefix: "passage: ",
},
{
model_name: "intfloat/multilingual-e5-small",
model_dim: 384,
normalize: true,
description:
"If you have many documents in other languages besides English, and you're running on a resource constrained system, then this is the one to go for.",
link: "https://huggingface.co/intfloat/multilingual-e5-base",
query_prefix: "query: ",
passage_prefix: "passage: ",
},
];
export const INVALID_OLD_MODEL = "thenlper/gte-small";
export function checkModelNameIsValid(modelName: string | undefined | null) {
if (!modelName) {
return false;
}
if (modelName === INVALID_OLD_MODEL) {
return false;
}
return true;
}

View File

@@ -0,0 +1,303 @@
"use client";
import { LoadingAnimation, ThreeDotsLoader } 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";
import { errorHandlingFetcher, fetcher } from "@/lib/fetcher";
import { Button, Divider, Text, Title } from "@tremor/react";
import { FiCpu, FiPackage } from "react-icons/fi";
import useSWR, { mutate } from "swr";
import { ModelOption, ModelSelector } from "./ModelSelector";
import { useState } from "react";
import { ModelSelectionConfirmaionModal } from "./ModelSelectionConfirmation";
import { ReindexingProgressTable } from "./ReindexingProgressTable";
import { Modal } from "@/components/Modal";
import {
AVAILABLE_MODELS,
EmbeddingModelResponse,
INVALID_OLD_MODEL,
} from "./embeddingModels";
import { ErrorCallout } from "@/components/ErrorCallout";
import { Connector, ConnectorIndexingStatus } from "@/lib/types";
import Link from "next/link";
function Main() {
const [tentativeNewEmbeddingModel, setTentativeNewEmbeddingModel] = useState<
string | null
>(null);
const [isCancelling, setIsCancelling] = useState<boolean>(false);
const [showAddConnectorPopup, setShowAddConnectorPopup] =
useState<boolean>(false);
const {
data: currentEmeddingModel,
isLoading: isLoadingCurrentModel,
error: currentEmeddingModelError,
} = useSWR<EmbeddingModelResponse>(
"/api/secondary-index/get-current-embedding-model",
errorHandlingFetcher,
{ refreshInterval: 5000 } // 5 seconds
);
const {
data: futureEmeddingModel,
isLoading: isLoadingFutureModel,
error: futureEmeddingModelError,
} = useSWR<EmbeddingModelResponse>(
"/api/secondary-index/get-secondary-embedding-model",
errorHandlingFetcher,
{ refreshInterval: 5000 } // 5 seconds
);
const {
data: ongoingReIndexingStatus,
isLoading: isLoadingOngoingReIndexingStatus,
} = useSWR<ConnectorIndexingStatus<any, any>[]>(
"/api/manage/admin/connector/indexing-status?secondary_index=true",
errorHandlingFetcher,
{ refreshInterval: 5000 } // 5 seconds
);
const { data: connectors } = useSWR<Connector<any>[]>(
"/api/manage/connector",
errorHandlingFetcher,
{ refreshInterval: 5000 } // 5 seconds
);
const onSelect = async (modelName: string) => {
if (currentEmeddingModel?.model_name === INVALID_OLD_MODEL) {
await onConfirm(modelName);
} else {
setTentativeNewEmbeddingModel(modelName);
}
};
const onConfirm = async (modelName: string) => {
const modelDescriptor = AVAILABLE_MODELS.find(
(model) => model.model_name === modelName
);
const response = await fetch(
"/api/secondary-index/set-new-embedding-model",
{
method: "POST",
body: JSON.stringify(modelDescriptor),
headers: {
"Content-Type": "application/json",
},
}
);
if (response.ok) {
setTentativeNewEmbeddingModel(null);
mutate("/api/secondary-index/get-secondary-embedding-model");
if (!connectors || !connectors.length) {
setShowAddConnectorPopup(true);
}
} else {
alert(`Failed to update embedding model - ${await response.text()}`);
}
};
const onCancel = async () => {
const response = await fetch("/api/secondary-index/cancel-new-embedding", {
method: "POST",
});
if (response.ok) {
setTentativeNewEmbeddingModel(null);
mutate("/api/secondary-index/get-secondary-embedding-model");
} else {
alert(
`Failed to cancel embedding model update - ${await response.text()}`
);
}
setIsCancelling(false);
};
if (isLoadingCurrentModel || isLoadingFutureModel) {
return <ThreeDotsLoader />;
}
if (
currentEmeddingModelError ||
!currentEmeddingModel ||
futureEmeddingModelError ||
!futureEmeddingModel
) {
return <ErrorCallout errorTitle="Failed to fetch embedding model status" />;
}
const currentModelName = currentEmeddingModel.model_name;
const currentModel = AVAILABLE_MODELS.find(
(model) => model.model_name === currentModelName
);
const newModelSelection = AVAILABLE_MODELS.find(
(model) => model.model_name === futureEmeddingModel.model_name
);
return (
<div>
{tentativeNewEmbeddingModel && (
<ModelSelectionConfirmaionModal
selectedModel={tentativeNewEmbeddingModel}
onConfirm={() => onConfirm(tentativeNewEmbeddingModel)}
onCancel={() => setTentativeNewEmbeddingModel(null)}
/>
)}
{showAddConnectorPopup && (
<Modal width={"w-[600px]"}>
<div>
<div>
<b>Embeding model successfully selected</b> 🙌
<br />
<br />
To complete the initial setup, let&apos;s add a connector.
</div>
<div className="flex">
<Link className="mx-auto mt-2 w-fit" href="/admin/add-connector">
<Button className="mt-3 mx-auto" size="xs">
Add Connector
</Button>
</Link>
</div>
</div>
</Modal>
)}
{isCancelling && (
<Modal
onOutsideClick={() => setIsCancelling(false)}
title="Cancel Embedding Model Switch"
>
<div>
<div>
Are you sure you want to cancel?
<br />
<br />
Cancelling will revert to the previous model and all progress will
be lost.
</div>
<div className="flex">
<Button onClick={onCancel} className="mt-3 mx-auto" color="green">
Confirm
</Button>
</div>
</div>
</Modal>
)}
<Text>
Embedding models are used to generate embeddings for your documents,
which then power Danswer&apos;s search.
</Text>
{currentModel ? (
<>
<Title className="mt-8 mb-2">Current Embedding Model</Title>
<Text>
<ModelOption model={currentModel} />
</Text>
</>
) : (
newModelSelection &&
(!connectors || !connectors.length) && (
<>
<Title className="mt-8 mb-2">Current Embedding Model</Title>
<Text>
<ModelOption model={newModelSelection} />
</Text>
</>
)
)}
{!newModelSelection ? (
<div>
{currentModel ? (
<>
<Title className="mt-8">Switch your Embedding Model</Title>
<Text className="mb-4">
If the current model is not working for you, you can update your
model choice below. Note that this will require a complete
re-indexing of all your documents across every connected source.
We will take care of this in the background, but depending on
the size of your corpus, this could take hours, day, or even
weeks. You can monitor the progress of the re-indexing on this
page.
</Text>
</>
) : (
<>
<Title className="mt-8 mb-4">Choose your Embedding Model</Title>
</>
)}
<ModelSelector
modelOptions={AVAILABLE_MODELS.filter(
(modelOption) => modelOption.model_name !== currentModelName
)}
setSelectedModel={onSelect}
/>
</div>
) : (
connectors &&
connectors.length > 0 && (
<div>
<Title className="mt-8">Current Upgrade Status</Title>
<div className="mt-4">
<div className="italic text-sm mb-2">
Currently in the process of switching to:
</div>
<ModelOption model={newModelSelection} />
<Button
color="red"
size="xs"
className="mt-4"
onClick={() => setIsCancelling(true)}
>
Cancel
</Button>
<Text className="my-4">
The table below shows the re-indexing progress of all existing
connectors. Once all connectors have been re-indexed, the new
model will be used for all search queries. Until then, we will
use the old model so that no downtime is necessary during this
transition.
</Text>
{isLoadingOngoingReIndexingStatus ? (
<ThreeDotsLoader />
) : ongoingReIndexingStatus ? (
<ReindexingProgressTable
reindexingProgress={ongoingReIndexingStatus}
/>
) : (
<ErrorCallout errorTitle="Failed to fetch re-indexing progress" />
)}
</div>
</div>
)
)}
</div>
);
}
function Page() {
return (
<div className="mx-auto container">
<AdminPageTitle
title="Embedding"
icon={<FiPackage size={32} className="my-auto" />}
/>
<Main />
</div>
);
}
export default Page;

View File

@@ -38,7 +38,7 @@ function AllPersonaOptionDisplay({
}) {
return (
<Modal onOutsideClick={handleClose}>
<div className="px-8 py-12">
<div>
<div className="flex w-full border-b border-border mb-4 pb-4">
<h2 className="text-xl text-strong font-bold flex">
<div className="p-1 bg-ai rounded-lg h-fit my-auto mr-2">

View File

@@ -6,12 +6,7 @@ import {
import { redirect } from "next/navigation";
import { fetchSS } from "@/lib/utilsSS";
import { Connector, DocumentSet, Tag, User, ValidSources } from "@/lib/types";
import {
BackendMessage,
ChatSession,
Message,
RetrievalType,
} from "./interfaces";
import { ChatSession } from "./interfaces";
import { unstable_noStore as noStore } from "next/cache";
import { Persona } from "../admin/personas/interfaces";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
@@ -21,6 +16,11 @@ import { cookies } from "next/headers";
import { DOCUMENT_SIDEBAR_WIDTH_COOKIE_NAME } from "@/components/resizable/contants";
import { personaComparator } from "../admin/personas/lib";
import { ChatLayout } from "./ChatPage";
import {
EmbeddingModelResponse,
checkModelNameIsValid,
} from "../admin/models/embedding/embeddingModels";
import { SwitchModelModal } from "@/components/SwitchModelModal";
export default async function Page({
searchParams,
@@ -37,20 +37,19 @@ export default async function Page({
fetchSS("/persona?include_default=true"),
fetchSS("/chat/get-user-chat-sessions"),
fetchSS("/query/valid-tags"),
fetchSS("/secondary-index/get-current-embedding-model"),
];
// catch cases where the backend is completely unreachable here
// without try / catch, will just raise an exception and the page
// will not render
let results: (User | Response | AuthTypeMetadata | null)[] = [
null,
null,
null,
null,
null,
null,
null,
];
let results: (
| User
| Response
| AuthTypeMetadata
| EmbeddingModelResponse
| null
)[] = [null, null, null, null, null, null, null, null];
try {
results = await Promise.all(tasks);
} catch (e) {
@@ -63,6 +62,7 @@ export default async function Page({
const personasResponse = results[4] as Response | null;
const chatSessionsResponse = results[5] as Response | null;
const tagsResponse = results[6] as Response | null;
const embeddingModelResponse = results[7] as Response | null;
const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) {
@@ -124,6 +124,11 @@ export default async function Page({
console.log(`Failed to fetch tags - ${tagsResponse?.status}`);
}
const embeddingModelName =
embeddingModelResponse && embeddingModelResponse.ok
? ((await embeddingModelResponse.json()).model_name as string)
: null;
const defaultPersonaIdRaw = searchParams["personaId"];
const defaultPersonaId = defaultPersonaIdRaw
? parseInt(defaultPersonaIdRaw)
@@ -141,7 +146,13 @@ export default async function Page({
<InstantSSRAutoRefresh />
<ApiKeyModal />
{connectors.length === 0 && <WelcomeModal />}
{connectors.length === 0 && (
<WelcomeModal embeddingModelName={embeddingModelName} />
)}
{!checkModelNameIsValid(embeddingModelName) && (
<SwitchModelModal embeddingModelName={embeddingModelName} />
)}
<ChatLayout
user={user}

View File

@@ -17,6 +17,8 @@ import { WelcomeModal } from "@/components/WelcomeModal";
import { unstable_noStore as noStore } from "next/cache";
import { InstantSSRAutoRefresh } from "@/components/SSRAutoRefresh";
import { personaComparator } from "../admin/personas/lib";
import { checkModelNameIsValid } from "../admin/models/embedding/embeddingModels";
import { SwitchModelModal } from "@/components/SwitchModelModal";
export default async function Home() {
// Disable caching so we always get the up to date connector / document set / persona info
@@ -31,6 +33,7 @@ export default async function Home() {
fetchSS("/manage/document-set"),
fetchSS("/persona"),
fetchSS("/query/valid-tags"),
fetchSS("/secondary-index/get-current-embedding-model"),
];
// catch cases where the backend is completely unreachable here
@@ -43,6 +46,7 @@ export default async function Home() {
null,
null,
null,
null,
];
try {
results = await Promise.all(tasks);
@@ -55,6 +59,7 @@ export default async function Home() {
const documentSetsResponse = results[3] as Response | null;
const personaResponse = results[4] as Response | null;
const tagsResponse = results[5] as Response | null;
const embeddingModelResponse = results[6] as Response | null;
const authDisabled = authTypeMetadata?.authType === "disabled";
if (!authDisabled && !user) {
@@ -99,6 +104,11 @@ export default async function Home() {
console.log(`Failed to fetch tags - ${tagsResponse?.status}`);
}
const embeddingModelName =
embeddingModelResponse && embeddingModelResponse.ok
? ((await embeddingModelResponse.json()).model_name as string)
: null;
// needs to be done in a non-client side component due to nextjs
const storedSearchType = cookies().get("searchType")?.value as
| string
@@ -117,7 +127,15 @@ export default async function Home() {
</div>
<ApiKeyModal />
<InstantSSRAutoRefresh />
{connectors.length === 0 && connectorsResponse?.ok && <WelcomeModal />}
{connectors.length === 0 && connectorsResponse?.ok && (
<WelcomeModal embeddingModelName={embeddingModelName} />
)}
{!checkModelNameIsValid(embeddingModelName) && (
<SwitchModelModal embeddingModelName={embeddingModelName} />
)}
<div className="px-24 pt-10 flex flex-col items-center min-h-screen">
<div className="w-full">
<SearchSection

View File

@@ -1,8 +1,12 @@
import { Divider } from "@tremor/react";
import { FiX } from "react-icons/fi";
interface ModalProps {
children: JSX.Element | string;
title?: JSX.Element | string;
onOutsideClick?: () => void;
className?: string;
width?: string;
}
export function Modal({
@@ -10,6 +14,7 @@ export function Modal({
title,
onOutsideClick,
className,
width,
}: ModalProps) {
return (
<div>
@@ -23,15 +28,26 @@ export function Modal({
<div
className={`
bg-background rounded shadow-lg
relative w-1/2 text-sm
relative ${width ?? "w-1/2"} text-sm p-8
${className}
`}
onClick={(event) => event.stopPropagation()}
>
{title && (
<h2 className="text-xl font-bold mb-3 border-b border-border pt-4 pb-3 bg-background-strong px-6">
{title}
</h2>
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">{title}</h2>
{onOutsideClick && (
<div
onClick={onOutsideClick}
className="my-auto ml-auto p-2 hover:bg-hover rounded cursor-pointer"
>
<FiX size={20} />
</div>
)}
</div>
<Divider />
</>
)}
{children}
</div>

View File

@@ -0,0 +1,40 @@
"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 SwitchModelModal({
embeddingModelName,
}: {
embeddingModelName: null | string;
}) {
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">
Switch Embedding Model
</h2>
<Text>
We&apos;ve detected you are using our old default embedding model (
<i>{embeddingModelName || "thenlper/gte-small"}</i>). We believe that
search performance can be dramatically improved by a simple model
switch.
<br />
<br />
Please click the button below to choose a new model. Don&apos;t worry,
the re-indexing necessary for the switch will happen in the background
- your use of Danswer will not be interrupted.
</Text>
<div className="flex mt-4">
<Link href="/admin/models/embedding" className="w-fit mx-auto">
<Button size="xs">Choose your Embedding Model</Button>
</Link>
</div>
</div>
</Modal>
);
}

View File

@@ -1,34 +1,68 @@
"use client";
import { Button } from "@tremor/react";
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: null | string;
}) {
const validModelSelected = checkModelNameIsValid(embeddingModelName);
export function WelcomeModal() {
return (
<Modal className="max-w-4xl">
<div className="px-8 py-6">
<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 className="mb-4">
<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>
<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-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/add-connector" className="mx-auto">
<Button>Setup your first connector!</Button>
<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>

View File

@@ -10,6 +10,7 @@ import {
ZoomInIcon,
RobotIcon,
ConnectorIcon,
SlackIcon,
} from "@/components/icons/icons";
import { User } from "@/lib/types";
import {
@@ -18,6 +19,7 @@ import {
getCurrentUserSS,
} from "@/lib/userSS";
import { redirect } from "next/navigation";
import { FiCpu, FiLayers, FiPackage, FiSlack } from "react-icons/fi";
export async function Layout({ children }: { children: React.ReactNode }) {
const tasks = [getAuthTypeMetadataSS(), getCurrentUserSS()];
@@ -128,7 +130,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
{
name: (
<div className="flex">
<CPUIcon size={18} />
<FiSlack size={18} />
<div className="ml-1">Slack Bots</div>
</div>
),
@@ -137,17 +139,26 @@ export async function Layout({ children }: { children: React.ReactNode }) {
],
},
{
name: "Keys",
name: "Model Configs",
items: [
{
name: (
<div className="flex">
<KeyIcon size={18} />
<div className="ml-1">OpenAI</div>
<FiCpu size={18} />
<div className="ml-1">LLM</div>
</div>
),
link: "/admin/keys/openai",
},
{
name: (
<div className="flex">
<FiPackage size={18} />
<div className="ml-1">Embedding</div>
</div>
),
link: "/admin/models/embedding",
},
],
},
{

View File

@@ -25,7 +25,7 @@ export const ApiKeyModal = () => {
return (
<Modal className="max-w-4xl" onOutsideClick={() => setIsOpen(false)}>
<div className="px-8 py-6">
<div>
<div>
<Text className="mb-2.5">
Can&apos;t find a valid registered OpenAI API key. Please provide