mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-26 11:58:28 +02:00
UI for model selection
This commit is contained in:
@@ -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 />
|
||||
|
@@ -31,7 +31,8 @@ function Main({ ccPairId }: { ccPairId: number }) {
|
||||
error,
|
||||
} = useSWR<CCPairFullInfo>(
|
||||
buildCCPairInfoUrl(ccPairId),
|
||||
errorHandlingFetcher
|
||||
errorHandlingFetcher,
|
||||
{ refreshInterval: 5000 } // 5 seconds
|
||||
);
|
||||
|
||||
if (isLoading) {
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
|
||||
|
@@ -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 />
|
||||
|
||||
|
@@ -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>
|
||||
);
|
||||
}
|
77
web/src/app/admin/models/embedding/ModelSelector.tsx
Normal file
77
web/src/app/admin/models/embedding/ModelSelector.tsx
Normal 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>
|
||||
);
|
||||
}
|
@@ -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>
|
||||
);
|
||||
}
|
73
web/src/app/admin/models/embedding/embeddingModels.ts
Normal file
73
web/src/app/admin/models/embedding/embeddingModels.ts
Normal 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;
|
||||
}
|
303
web/src/app/admin/models/embedding/page.tsx
Normal file
303
web/src/app/admin/models/embedding/page.tsx
Normal 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'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'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;
|
@@ -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">
|
||||
|
@@ -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}
|
||||
|
@@ -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
|
||||
|
@@ -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>
|
||||
|
40
web/src/components/SwitchModelModal.tsx
Normal file
40
web/src/components/SwitchModelModal.tsx
Normal 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'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'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>
|
||||
);
|
||||
}
|
@@ -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'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's various data sources. Once setup,
|
||||
we'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's search. Different models have different strengths,
|
||||
but don't worry we'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's various data
|
||||
sources. Once setup, we'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>
|
||||
|
@@ -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",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
@@ -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't find a valid registered OpenAI API key. Please provide
|
||||
|
Reference in New Issue
Block a user