[FEAT] Implement model provider UI updates (#996)

* implement new LLM preference UI

* implement new vector db preferences UI to match LLM preferences

* implement new embedding preferences UI to match LLM preferences

* normalize placeholder text for search input

* implement new transcription preferences UI to match LLM preferences

* remove uneeded css

* implement new UI for llm preference onboarding

* implement new UI for embedder preference onboarding

* implement new UI for vector db preference onboarding

* fix placeholder text

* unset onboarding

* move autocomplete field

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield 2024-04-02 09:56:14 -07:00 committed by GitHub
parent b643639d0f
commit 1cd9e1336b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 508 additions and 252 deletions

View File

@ -9,8 +9,8 @@ export default function EmbedderItem({
return (
<div
onClick={() => onClick(value)}
className={`w-full hover:bg-white/10 p-2 rounded-md hover:cursor-pointer ${
checked && "bg-white/10"
className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-white/10 ${
checked ? "bg-white/10" : ""
}`}
>
<input
@ -28,8 +28,8 @@ export default function EmbedderItem({
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col">
<div className="text-sm font-semibold">{name}</div>
<div className="mt-1 text-xs text-white/60">{description}</div>
<div className="text-sm font-semibold text-white">{name}</div>
<div className="mt-1 text-xs text-[#D2D5DB]">{description}</div>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
export default function NativeEmbeddingOptions() {
return (
<div className="w-full h-20 items-center justify-center flex">
<div className="w-full h-10 items-center flex">
<p className="text-sm font-base text-white text-opacity-60">
There is no set up required when using AnythingLLM's native embedding
engine.

View File

@ -9,8 +9,8 @@ export default function LLMItem({
return (
<div
onClick={() => onClick(value)}
className={`w-full hover:bg-white/10 p-2 rounded-md hover:cursor-pointer ${
checked && "bg-white/10"
className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-white/10 ${
checked ? "bg-white/10" : ""
}`}
>
<input
@ -28,8 +28,8 @@ export default function LLMItem({
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col">
<div className="text-sm font-semibold">{name}</div>
<div className="mt-1 text-xs text-white/60">{description}</div>
<div className="text-sm font-semibold text-white">{name}</div>
<div className="mt-1 text-xs text-[#D2D5DB]">{description}</div>
</div>
</div>
</div>

View File

@ -1,6 +1,6 @@
export default function LanceDBOptions() {
return (
<div className="w-full h-10 items-center justify-center flex">
<div className="w-full h-10 items-center flex">
<p className="text-sm font-base text-white text-opacity-60">
There is no configuration needed for LanceDB.
</p>

View File

@ -9,7 +9,7 @@ export default function VectorDBItem({
return (
<div
onClick={() => onClick(value)}
className={`w-full hover:bg-white/10 p-2 rounded-md hover:cursor-pointer ${
className={`w-full p-2 rounded-md hover:cursor-pointer hover:bg-white/10 ${
checked ? "bg-white/10" : ""
}`}
>
@ -28,8 +28,8 @@ export default function VectorDBItem({
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col">
<div className="text-sm font-semibold">{name}</div>
<div className="mt-1 text-xs text-white/60">{description}</div>
<div className="text-sm font-semibold text-white">{name}</div>
<div className="mt-1 text-xs text-[#D2D5DB]">{description}</div>
</div>
</div>
</div>

View File

@ -633,3 +633,32 @@ does not extend the close button beyond the viewport. */
.upload-modal-arrow {
margin-top: 25%;
}
/* Scrollbar container */
.white-scrollbar {
overflow-y: scroll;
scrollbar-width: thin;
scrollbar-color: #ffffff #18181b;
margin-right: 8px;
}
/* Webkit browsers (Chrome, Safari) */
.white-scrollbar::-webkit-scrollbar {
width: 3px;
background-color: #18181b;
}
.white-scrollbar::-webkit-scrollbar-track {
background-color: #18181b;
margin-right: 8px;
}
.white-scrollbar::-webkit-scrollbar-thumb {
background-color: #ffffff;
border-radius: 4px;
border: 2px solid #18181b;
}
.white-scrollbar::-webkit-scrollbar-thumb:hover {
background-color: #cccccc;
}

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import System from "@/models/system";
@ -16,7 +16,7 @@ import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions";
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper";
@ -29,6 +29,8 @@ export default function GeneralEmbeddingPreference() {
const [searchQuery, setSearchQuery] = useState("");
const [filteredEmbedders, setFilteredEmbedders] = useState([]);
const [selectedEmbedder, setSelectedEmbedder] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal();
const handleSubmit = async (e) => {
@ -65,10 +67,21 @@ export default function GeneralEmbeddingPreference() {
};
const updateChoice = (selection) => {
setSearchQuery("");
setSelectedEmbedder(selection);
setSearchMenuOpen(false);
setHasChanges(true);
};
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
@ -126,6 +139,10 @@ export default function GeneralEmbeddingPreference() {
setFilteredEmbedders(filtered);
}, [searchQuery, selectedEmbedder]);
const selectedEmbedderObject = EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
@ -174,55 +191,96 @@ export default function GeneralEmbeddingPreference() {
format which AnythingLLM can use to process.
</p>
</div>
<div className="text-sm font-medium text-white mt-6 mb-4">
Embedding Providers
<div className="text-base font-bold text-white mt-6 mb-4">
Embedding Provider
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search Embedding providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<div className="relative">
{searchMenuOpen && (
<div
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
onClick={() => setSearchMenuOpen(false)}
/>
)}
{searchMenuOpen ? (
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
<div className="w-full flex flex-col gap-y-1">
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
<MagnifyingGlass
size={20}
weight="bold"
className="absolute left-4 z-30 text-white -ml-4 my-2"
/>
<input
type="text"
name="embedder-search"
autoComplete="off"
placeholder="Search all embedding providers"
className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<X
size={20}
weight="bold"
className="cursor-pointer text-white hover:text-[#9CA3AF]"
onClick={handleXButton}
/>
</div>
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
{filteredEmbedders.map((embedder) => (
<EmbedderItem
key={embedder.name}
name={embedder.name}
value={embedder.value}
image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
))}
</div>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredEmbedders.map((embedder) => {
return (
<EmbedderItem
key={embedder.name}
name={embedder.name}
value={embedder.value}
image={embedder.logo}
description={embedder.description}
checked={selectedEmbedder === embedder.value}
onClick={() => updateChoice(embedder.value)}
/>
);
})}
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedEmbedder &&
EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
)?.options}
</div>
) : (
<button
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
type="button"
onClick={() => setSearchMenuOpen(true)}
>
<div className="flex gap-x-4 items-center">
<img
src={selectedEmbedderObject.logo}
alt={`${selectedEmbedderObject.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedEmbedderObject.name}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedEmbedderObject.description}
</div>
</div>
</div>
<CaretUpDown
size={24}
weight="bold"
className="text-white"
/>
</button>
)}
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedEmbedder &&
EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
)?.options}
</div>
</div>
</form>

View File

@ -1,4 +1,4 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useRef, useState } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import System from "@/models/system";
@ -34,7 +34,7 @@ import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
export default function GeneralLLMPreference() {
const [saving, setSaving] = useState(false);
@ -44,6 +44,8 @@ export default function GeneralLLMPreference() {
const [searchQuery, setSearchQuery] = useState("");
const [filteredLLMs, setFilteredLLMs] = useState([]);
const [selectedLLM, setSelectedLLM] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const isHosted = window.location.hostname.includes("useanything.com");
const handleSubmit = async (e) => {
@ -66,10 +68,21 @@ export default function GeneralLLMPreference() {
};
const updateLLMChoice = (selection) => {
setSearchQuery("");
setSelectedLLM(selection);
setSearchMenuOpen(false);
setHasChanges(true);
};
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
@ -193,6 +206,8 @@ export default function GeneralLLMPreference() {
},
];
const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
@ -234,54 +249,97 @@ export default function GeneralLLMPreference() {
properly.
</p>
</div>
<div className="text-sm font-medium text-white mt-6 mb-4">
LLM Providers
<div className="text-base font-bold text-white mt-6 mb-4">
LLM Provider
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search LLM providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<div className="relative">
{searchMenuOpen && (
<div
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
onClick={() => setSearchMenuOpen(false)}
/>
)}
{searchMenuOpen ? (
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
<div className="w-full flex flex-col gap-y-1">
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
<MagnifyingGlass
size={20}
weight="bold"
className="absolute left-4 z-30 text-white -ml-4 my-2"
/>
<input
type="text"
name="llm-search"
autoComplete="off"
placeholder="Search all LLM providers"
className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<X
size={20}
weight="bold"
className="cursor-pointer text-white hover:text-[#9CA3AF]"
onClick={handleXButton}
/>
</div>
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
{filteredLLMs.map((llm) => {
if (llm.value === "native" && isHosted) return null;
return (
<LLMItem
key={llm.name}
name={llm.name}
value={llm.value}
image={llm.logo}
description={llm.description}
checked={selectedLLM === llm.value}
onClick={() => updateLLMChoice(llm.value)}
/>
);
})}
</div>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredLLMs.map((llm) => {
if (llm.value === "native" && isHosted) return null;
return (
<LLMItem
key={llm.name}
name={llm.name}
value={llm.value}
image={llm.logo}
description={llm.description}
checked={selectedLLM === llm.value}
onClick={() => updateLLMChoice(llm.value)}
/>
);
})}
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedLLM &&
LLMS.find((llm) => llm.value === selectedLLM)?.options}
</div>
) : (
<button
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
type="button"
onClick={() => setSearchMenuOpen(true)}
>
<div className="flex gap-x-4 items-center">
<img
src={selectedLLMObject.logo}
alt={`${selectedLLMObject.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedLLMObject.name}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedLLMObject.description}
</div>
</div>
</div>
<CaretUpDown
size={24}
weight="bold"
className="text-white"
/>
</button>
)}
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedLLM &&
LLMS.find((llm) => llm.value === selectedLLM)?.options}
</div>
</div>
</form>

View File

@ -1,16 +1,15 @@
import React, { useEffect, useState } from "react";
import React, { useEffect, useState, useRef } from "react";
import { isMobile } from "react-device-detect";
import Sidebar from "@/components/SettingsSidebar";
import System from "@/models/system";
import showToast from "@/utils/toast";
import PreLoader from "@/components/Preloader";
import OpenAiLogo from "@/media/llmprovider/openai.png";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import OpenAiWhisperOptions from "@/components/TranscriptionSelection/OpenAiOptions";
import NativeTranscriptionOptions from "@/components/TranscriptionSelection/NativeTranscriptionOptions";
import LLMItem from "@/components/LLMSelection/LLMItem";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
export default function TranscriptionModelPreference() {
const [saving, setSaving] = useState(false);
@ -20,6 +19,8 @@ export default function TranscriptionModelPreference() {
const [searchQuery, setSearchQuery] = useState("");
const [filteredProviders, setFilteredProviders] = useState([]);
const [selectedProvider, setSelectedProvider] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const handleSubmit = async (e) => {
e.preventDefault();
@ -41,10 +42,21 @@ export default function TranscriptionModelPreference() {
};
const updateProviderChoice = (selection) => {
setSearchQuery("");
setSelectedProvider(selection);
setSearchMenuOpen(false);
setHasChanges(true);
};
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
@ -55,13 +67,6 @@ export default function TranscriptionModelPreference() {
fetchKeys();
}, []);
useEffect(() => {
const filtered = PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredProviders(filtered);
}, [searchQuery, selectedProvider]);
const PROVIDERS = [
{
name: "OpenAI",
@ -80,6 +85,17 @@ export default function TranscriptionModelPreference() {
},
];
useEffect(() => {
const filtered = PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredProviders(filtered);
}, [searchQuery, selectedProvider]);
const selectedProviderObject = PROVIDERS.find(
(provider) => provider.value === selectedProvider
);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar />
@ -121,55 +137,96 @@ export default function TranscriptionModelPreference() {
transcribe.
</p>
</div>
<div className="text-sm font-medium text-white mt-6 mb-4">
Transcription Providers
<div className="text-base font-bold text-white mt-6 mb-4">
Transcription Provider
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search audio transcription providers"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<div className="relative">
{searchMenuOpen && (
<div
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
onClick={() => setSearchMenuOpen(false)}
/>
)}
{searchMenuOpen ? (
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
<div className="w-full flex flex-col gap-y-1">
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
<MagnifyingGlass
size={20}
weight="bold"
className="absolute left-4 z-30 text-white -ml-4 my-2"
/>
<input
type="text"
name="provider-search"
autoComplete="off"
placeholder="Search audio transcription providers"
className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<X
size={20}
weight="bold"
className="cursor-pointer text-white hover:text-[#9CA3AF]"
onClick={handleXButton}
/>
</div>
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
{filteredProviders.map((provider) => (
<LLMItem
key={provider.name}
name={provider.name}
value={provider.value}
image={provider.logo}
description={provider.description}
checked={selectedProvider === provider.value}
onClick={() => updateProviderChoice(provider.value)}
/>
))}
</div>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredProviders.map((provider) => {
return (
<LLMItem
key={provider.name}
name={provider.name}
value={provider.value}
image={provider.logo}
description={provider.description}
checked={selectedProvider === provider.value}
onClick={() => updateProviderChoice(provider.value)}
/>
);
})}
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedProvider &&
PROVIDERS.find(
(provider) => provider.value === selectedProvider
)?.options}
</div>
) : (
<button
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
type="button"
onClick={() => setSearchMenuOpen(true)}
>
<div className="flex gap-x-4 items-center">
<img
src={selectedProviderObject.logo}
alt={`${selectedProviderObject.name} logo`}
className="w-10 h-10 rounded-md"
/>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedProviderObject.name}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedProviderObject.description}
</div>
</div>
</div>
<CaretUpDown
size={24}
weight="bold"
className="text-white"
/>
</button>
)}
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedProvider &&
PROVIDERS.find(
(provider) => provider.value === selectedProvider
)?.options}
</div>
</div>
</form>

View File

@ -1,4 +1,4 @@
import React, { useState, useEffect } from "react";
import React, { useState, useEffect, useRef } from "react";
import Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect";
import System from "@/models/system";
@ -13,7 +13,7 @@ import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import PreLoader from "@/components/Preloader";
import ChangeWarningModal from "@/components/ChangeWarning";
import { MagnifyingGlass } from "@phosphor-icons/react";
import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions";
import ChromaDBOptions from "@/components/VectorDBSelection/ChromaDBOptions";
import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions";
@ -35,8 +35,55 @@ export default function GeneralVectorDatabase() {
const [searchQuery, setSearchQuery] = useState("");
const [filteredVDBs, setFilteredVDBs] = useState([]);
const [selectedVDB, setSelectedVDB] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal();
const handleSubmit = async (e) => {
e.preventDefault();
if (selectedVDB !== settings?.VectorDB && hasChanges && hasEmbeddings) {
openModal();
} else {
await handleSaveSettings();
}
};
const handleSaveSettings = async () => {
setSaving(true);
const form = document.getElementById("vectordb-form");
const settingsData = {};
const formData = new FormData(form);
settingsData.VectorDB = selectedVDB;
for (var [key, value] of formData.entries()) settingsData[key] = value;
const { error } = await System.updateSystem(settingsData);
if (error) {
showToast(`Failed to save vector database settings: ${error}`, "error");
setHasChanges(true);
} else {
showToast("Vector database preferences saved successfully.", "success");
setHasChanges(false);
}
setSaving(false);
closeModal();
};
const updateVectorChoice = (selection) => {
setSearchQuery("");
setSelectedVDB(selection);
setSearchMenuOpen(false);
setHasChanges(true);
};
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => {
async function fetchKeys() {
const _settings = await System.keys();
@ -48,6 +95,13 @@ export default function GeneralVectorDatabase() {
fetchKeys();
}, []);
useEffect(() => {
const filtered = VECTOR_DBS.filter((vdb) =>
vdb.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredVDBs(filtered);
}, [searchQuery, selectedVDB]);
const VECTOR_DBS = [
{
name: "LanceDB",
@ -111,46 +165,7 @@ export default function GeneralVectorDatabase() {
},
];
const updateVectorChoice = (selection) => {
setHasChanges(true);
setSelectedVDB(selection);
};
const handleSubmit = async (e) => {
e.preventDefault();
if (selectedVDB !== settings?.VectorDB && hasChanges && hasEmbeddings) {
openModal();
} else {
await handleSaveSettings();
}
};
const handleSaveSettings = async () => {
setSaving(true);
const form = document.getElementById("vectordb-form");
const settingsData = {};
const formData = new FormData(form);
settingsData.VectorDB = selectedVDB;
for (var [key, value] of formData.entries()) settingsData[key] = value;
const { error } = await System.updateSystem(settingsData);
if (error) {
showToast(`Failed to save vector database settings: ${error}`, "error");
setHasChanges(true);
} else {
showToast("Vector database preferences saved successfully.", "success");
setHasChanges(false);
}
setSaving(false);
closeModal();
};
useEffect(() => {
const filtered = VECTOR_DBS.filter((vdb) =>
vdb.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredVDBs(filtered);
}, [searchQuery, selectedVDB]);
const selectedVDBObject = VECTOR_DBS.find((vdb) => vdb.value === selectedVDB);
return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
@ -176,7 +191,7 @@ export default function GeneralVectorDatabase() {
>
<div className="flex flex-col w-full px-1 md:pl-6 md:pr-[86px] md:py-6 py-16">
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
<div className="flex items-center gap-x-4">
<div className="flex gap-x-4 items-center">
<p className="text-lg leading-6 font-bold text-white">
Vector Database
</p>
@ -196,55 +211,94 @@ export default function GeneralVectorDatabase() {
are current and correct.
</p>
</div>
<div className="text-sm font-medium text-white mt-6 mb-4">
Vector Database Providers
<div className="text-base font-bold text-white mt-6 mb-4">
Vector Database Provider
</div>
<div className="w-full">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white">
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm">
<div className="w-full flex items-center sticky top-0">
<MagnifyingGlass
size={16}
weight="bold"
className="absolute left-4 z-30 text-white"
/>
<input
type="text"
placeholder="Search vector databases"
className="bg-zinc-600 z-20 pl-10 h-[38px] rounded-full w-full px-4 py-1 text-sm border-2 border-slate-300/40 outline-none focus:border-white text-white"
onChange={(e) => {
e.preventDefault();
setSearchQuery(e.target.value);
}}
autoComplete="off"
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<div className="relative">
{searchMenuOpen && (
<div
className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
onClick={() => setSearchMenuOpen(false)}
/>
)}
{searchMenuOpen ? (
<div className="absolute top-0 left-0 w-full max-w-[640px] max-h-[310px] overflow-auto white-scrollbar min-h-[64px] bg-[#18181B] rounded-lg flex flex-col justify-between cursor-pointer border-2 border-[#46C8FF] z-20">
<div className="w-full flex flex-col gap-y-1">
<div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
<MagnifyingGlass
size={20}
weight="bold"
className="absolute left-4 z-30 text-white -ml-4 my-2"
/>
<input
type="text"
name="vdb-search"
autoComplete="off"
placeholder="Search all vector database providers"
className="-ml-4 my-2 bg-transparent z-20 pl-12 h-[38px] w-full px-4 py-1 text-sm outline-none focus:border-white text-white placeholder:text-white placeholder:font-medium"
onChange={(e) => setSearchQuery(e.target.value)}
ref={searchInputRef}
onKeyDown={(e) => {
if (e.key === "Enter") e.preventDefault();
}}
/>
<X
size={20}
weight="bold"
className="cursor-pointer text-white hover:text-[#9CA3AF]"
onClick={handleXButton}
/>
</div>
<div className="flex-1 pl-4 pr-2 flex flex-col gap-y-1 overflow-y-auto white-scrollbar pb-4">
{filteredVDBs.map((vdb) => (
<VectorDBItem
key={vdb.name}
name={vdb.name}
value={vdb.value}
image={vdb.logo}
description={vdb.description}
checked={selectedVDB === vdb.value}
onClick={() => updateVectorChoice(vdb.value)}
/>
))}
</div>
</div>
</div>
<div className="px-4 pt-[70px] flex flex-col gap-y-1 max-h-[390px] overflow-y-auto no-scroll pb-4">
{filteredVDBs.map((vdb) => (
<VectorDBItem
key={vdb.name}
name={vdb.name}
value={vdb.value}
image={vdb.logo}
description={vdb.description}
checked={selectedVDB === vdb.value}
onClick={() => updateVectorChoice(vdb.value)}
) : (
<button
className="w-full max-w-[640px] h-[64px] bg-[#18181B] rounded-lg flex items-center p-[14px] justify-between cursor-pointer border-2 border-transparent hover:border-[#46C8FF] transition-all duration-300"
type="button"
onClick={() => setSearchMenuOpen(true)}
>
<div className="flex gap-x-4 items-center">
<img
src={selectedVDBObject.logo}
alt={`${selectedVDBObject.name} logo`}
className="w-10 h-10 rounded-md"
/>
))}
</div>
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedVDB &&
VECTOR_DBS.find((vdb) => vdb.value === selectedVDB)
?.options}
</div>
<div className="flex flex-col text-left">
<div className="text-sm font-semibold text-white">
{selectedVDBObject.name}
</div>
<div className="mt-1 text-xs text-[#D2D5DB]">
{selectedVDBObject.description}
</div>
</div>
</div>
<CaretUpDown
size={24}
weight="bold"
className="text-white"
/>
</button>
)}
</div>
<div
onChange={() => setHasChanges(true)}
className="mt-4 flex flex-col gap-y-1"
>
{selectedVDB &&
VECTOR_DBS.find((vdb) => vdb.value === selectedVDB)?.options}
</div>
</div>
</form>