[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 ( return (
<div <div
onClick={() => onClick(value)} 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" checked ? "bg-white/10" : ""
}`} }`}
> >
<input <input
@ -28,8 +28,8 @@ export default function EmbedderItem({
className="w-10 h-10 rounded-md" className="w-10 h-10 rounded-md"
/> />
<div className="flex flex-col"> <div className="flex flex-col">
<div className="text-sm font-semibold">{name}</div> <div className="text-sm font-semibold text-white">{name}</div>
<div className="mt-1 text-xs text-white/60">{description}</div> <div className="mt-1 text-xs text-[#D2D5DB]">{description}</div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,6 +1,6 @@
export default function NativeEmbeddingOptions() { export default function NativeEmbeddingOptions() {
return ( 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"> <p className="text-sm font-base text-white text-opacity-60">
There is no set up required when using AnythingLLM's native embedding There is no set up required when using AnythingLLM's native embedding
engine. engine.

View File

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

View File

@ -1,6 +1,6 @@
export default function LanceDBOptions() { export default function LanceDBOptions() {
return ( 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"> <p className="text-sm font-base text-white text-opacity-60">
There is no configuration needed for LanceDB. There is no configuration needed for LanceDB.
</p> </p>

View File

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

View File

@ -633,3 +633,32 @@ does not extend the close button beyond the viewport. */
.upload-modal-arrow { .upload-modal-arrow {
margin-top: 25%; 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 Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import System from "@/models/system"; import System from "@/models/system";
@ -16,7 +16,7 @@ import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions"; import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions"; import OllamaEmbeddingOptions from "@/components/EmbeddingSelection/OllamaOptions";
import EmbedderItem from "@/components/EmbeddingSelection/EmbedderItem"; 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 { useModal } from "@/hooks/useModal";
import ModalWrapper from "@/components/ModalWrapper"; import ModalWrapper from "@/components/ModalWrapper";
@ -29,6 +29,8 @@ export default function GeneralEmbeddingPreference() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [filteredEmbedders, setFilteredEmbedders] = useState([]); const [filteredEmbedders, setFilteredEmbedders] = useState([]);
const [selectedEmbedder, setSelectedEmbedder] = useState(null); const [selectedEmbedder, setSelectedEmbedder] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal(); const { isOpen, openModal, closeModal } = useModal();
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
@ -65,10 +67,21 @@ export default function GeneralEmbeddingPreference() {
}; };
const updateChoice = (selection) => { const updateChoice = (selection) => {
setSearchQuery("");
setSelectedEmbedder(selection); setSelectedEmbedder(selection);
setSearchMenuOpen(false);
setHasChanges(true); setHasChanges(true);
}; };
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => { useEffect(() => {
async function fetchKeys() { async function fetchKeys() {
const _settings = await System.keys(); const _settings = await System.keys();
@ -126,6 +139,10 @@ export default function GeneralEmbeddingPreference() {
setFilteredEmbedders(filtered); setFilteredEmbedders(filtered);
}, [searchQuery, selectedEmbedder]); }, [searchQuery, selectedEmbedder]);
const selectedEmbedderObject = EMBEDDERS.find(
(embedder) => embedder.value === selectedEmbedder
);
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -174,55 +191,96 @@ export default function GeneralEmbeddingPreference() {
format which AnythingLLM can use to process. format which AnythingLLM can use to process.
</p> </p>
</div> </div>
<div className="text-sm font-medium text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
Embedding Providers Embedding Provider
</div> </div>
<div className="w-full"> <div className="relative">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white"> {searchMenuOpen && (
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm"> <div
<div className="w-full flex items-center sticky top-0"> className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
<MagnifyingGlass onClick={() => setSearchMenuOpen(false)}
size={16} />
weight="bold" )}
className="absolute left-4 z-30 text-white" {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">
<input <div className="w-full flex flex-col gap-y-1">
type="text" <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
placeholder="Search Embedding providers" <MagnifyingGlass
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" size={20}
onChange={(e) => setSearchQuery(e.target.value)} weight="bold"
autoComplete="off" className="absolute left-4 z-30 text-white -ml-4 my-2"
onKeyDown={(e) => { />
if (e.key === "Enter") e.preventDefault(); <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> </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) => { <button
return ( 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"
<EmbedderItem type="button"
key={embedder.name} onClick={() => setSearchMenuOpen(true)}
name={embedder.name} >
value={embedder.value} <div className="flex gap-x-4 items-center">
image={embedder.logo} <img
description={embedder.description} src={selectedEmbedderObject.logo}
checked={selectedEmbedder === embedder.value} alt={`${selectedEmbedderObject.name} logo`}
onClick={() => updateChoice(embedder.value)} className="w-10 h-10 rounded-md"
/> />
); <div className="flex flex-col text-left">
})} <div className="text-sm font-semibold text-white">
</div> {selectedEmbedderObject.name}
</div> </div>
<div <div className="mt-1 text-xs text-[#D2D5DB]">
onChange={() => setHasChanges(true)} {selectedEmbedderObject.description}
className="mt-4 flex flex-col gap-y-1" </div>
> </div>
{selectedEmbedder && </div>
EMBEDDERS.find( <CaretUpDown
(embedder) => embedder.value === selectedEmbedder size={24}
)?.options} weight="bold"
</div> 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>
</div> </div>
</form> </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 Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import System from "@/models/system"; import System from "@/models/system";
@ -34,7 +34,7 @@ import OpenRouterOptions from "@/components/LLMSelection/OpenRouterOptions";
import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions"; import GroqAiOptions from "@/components/LLMSelection/GroqAiOptions";
import LLMItem from "@/components/LLMSelection/LLMItem"; import LLMItem from "@/components/LLMSelection/LLMItem";
import { MagnifyingGlass } from "@phosphor-icons/react"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
export default function GeneralLLMPreference() { export default function GeneralLLMPreference() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -44,6 +44,8 @@ export default function GeneralLLMPreference() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [filteredLLMs, setFilteredLLMs] = useState([]); const [filteredLLMs, setFilteredLLMs] = useState([]);
const [selectedLLM, setSelectedLLM] = useState(null); const [selectedLLM, setSelectedLLM] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const isHosted = window.location.hostname.includes("useanything.com"); const isHosted = window.location.hostname.includes("useanything.com");
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
@ -66,10 +68,21 @@ export default function GeneralLLMPreference() {
}; };
const updateLLMChoice = (selection) => { const updateLLMChoice = (selection) => {
setSearchQuery("");
setSelectedLLM(selection); setSelectedLLM(selection);
setSearchMenuOpen(false);
setHasChanges(true); setHasChanges(true);
}; };
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => { useEffect(() => {
async function fetchKeys() { async function fetchKeys() {
const _settings = await System.keys(); const _settings = await System.keys();
@ -193,6 +206,8 @@ export default function GeneralLLMPreference() {
}, },
]; ];
const selectedLLMObject = LLMS.find((llm) => llm.value === selectedLLM);
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -234,54 +249,97 @@ export default function GeneralLLMPreference() {
properly. properly.
</p> </p>
</div> </div>
<div className="text-sm font-medium text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
LLM Providers LLM Provider
</div> </div>
<div className="w-full"> <div className="relative">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white"> {searchMenuOpen && (
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm"> <div
<div className="w-full flex items-center sticky top-0"> className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
<MagnifyingGlass onClick={() => setSearchMenuOpen(false)}
size={16} />
weight="bold" )}
className="absolute left-4 z-30 text-white" {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">
<input <div className="w-full flex flex-col gap-y-1">
type="text" <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
placeholder="Search LLM providers" <MagnifyingGlass
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" size={20}
onChange={(e) => setSearchQuery(e.target.value)} weight="bold"
autoComplete="off" className="absolute left-4 z-30 text-white -ml-4 my-2"
onKeyDown={(e) => { />
if (e.key === "Enter") e.preventDefault(); <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> </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) => { <button
if (llm.value === "native" && isHosted) return null; 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"
return ( type="button"
<LLMItem onClick={() => setSearchMenuOpen(true)}
key={llm.name} >
name={llm.name} <div className="flex gap-x-4 items-center">
value={llm.value} <img
image={llm.logo} src={selectedLLMObject.logo}
description={llm.description} alt={`${selectedLLMObject.name} logo`}
checked={selectedLLM === llm.value} className="w-10 h-10 rounded-md"
onClick={() => updateLLMChoice(llm.value)} />
/> <div className="flex flex-col text-left">
); <div className="text-sm font-semibold text-white">
})} {selectedLLMObject.name}
</div> </div>
</div> <div className="mt-1 text-xs text-[#D2D5DB]">
<div {selectedLLMObject.description}
onChange={() => setHasChanges(true)} </div>
className="mt-4 flex flex-col gap-y-1" </div>
> </div>
{selectedLLM && <CaretUpDown
LLMS.find((llm) => llm.value === selectedLLM)?.options} size={24}
</div> 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>
</div> </div>
</form> </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 { isMobile } from "react-device-detect";
import Sidebar from "@/components/SettingsSidebar"; import Sidebar from "@/components/SettingsSidebar";
import System from "@/models/system"; import System from "@/models/system";
import showToast from "@/utils/toast"; import showToast from "@/utils/toast";
import PreLoader from "@/components/Preloader"; import PreLoader from "@/components/Preloader";
import OpenAiLogo from "@/media/llmprovider/openai.png"; import OpenAiLogo from "@/media/llmprovider/openai.png";
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
import OpenAiWhisperOptions from "@/components/TranscriptionSelection/OpenAiOptions"; import OpenAiWhisperOptions from "@/components/TranscriptionSelection/OpenAiOptions";
import NativeTranscriptionOptions from "@/components/TranscriptionSelection/NativeTranscriptionOptions"; import NativeTranscriptionOptions from "@/components/TranscriptionSelection/NativeTranscriptionOptions";
import LLMItem from "@/components/LLMSelection/LLMItem"; import LLMItem from "@/components/LLMSelection/LLMItem";
import { MagnifyingGlass } from "@phosphor-icons/react"; import { CaretUpDown, MagnifyingGlass, X } from "@phosphor-icons/react";
export default function TranscriptionModelPreference() { export default function TranscriptionModelPreference() {
const [saving, setSaving] = useState(false); const [saving, setSaving] = useState(false);
@ -20,6 +19,8 @@ export default function TranscriptionModelPreference() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [filteredProviders, setFilteredProviders] = useState([]); const [filteredProviders, setFilteredProviders] = useState([]);
const [selectedProvider, setSelectedProvider] = useState(null); const [selectedProvider, setSelectedProvider] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const handleSubmit = async (e) => { const handleSubmit = async (e) => {
e.preventDefault(); e.preventDefault();
@ -41,10 +42,21 @@ export default function TranscriptionModelPreference() {
}; };
const updateProviderChoice = (selection) => { const updateProviderChoice = (selection) => {
setSearchQuery("");
setSelectedProvider(selection); setSelectedProvider(selection);
setSearchMenuOpen(false);
setHasChanges(true); setHasChanges(true);
}; };
const handleXButton = () => {
if (searchQuery.length > 0) {
setSearchQuery("");
if (searchInputRef.current) searchInputRef.current.value = "";
} else {
setSearchMenuOpen(!searchMenuOpen);
}
};
useEffect(() => { useEffect(() => {
async function fetchKeys() { async function fetchKeys() {
const _settings = await System.keys(); const _settings = await System.keys();
@ -55,13 +67,6 @@ export default function TranscriptionModelPreference() {
fetchKeys(); fetchKeys();
}, []); }, []);
useEffect(() => {
const filtered = PROVIDERS.filter((provider) =>
provider.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredProviders(filtered);
}, [searchQuery, selectedProvider]);
const PROVIDERS = [ const PROVIDERS = [
{ {
name: "OpenAI", 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 ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <div className="w-screen h-screen overflow-hidden bg-sidebar flex">
<Sidebar /> <Sidebar />
@ -121,55 +137,96 @@ export default function TranscriptionModelPreference() {
transcribe. transcribe.
</p> </p>
</div> </div>
<div className="text-sm font-medium text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
Transcription Providers Transcription Provider
</div> </div>
<div className="w-full"> <div className="relative">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white"> {searchMenuOpen && (
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm"> <div
<div className="w-full flex items-center sticky top-0"> className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
<MagnifyingGlass onClick={() => setSearchMenuOpen(false)}
size={16} />
weight="bold" )}
className="absolute left-4 z-30 text-white" {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">
<input <div className="w-full flex flex-col gap-y-1">
type="text" <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
placeholder="Search audio transcription providers" <MagnifyingGlass
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" size={20}
onChange={(e) => setSearchQuery(e.target.value)} weight="bold"
autoComplete="off" className="absolute left-4 z-30 text-white -ml-4 my-2"
onKeyDown={(e) => { />
if (e.key === "Enter") e.preventDefault(); <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> </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) => { <button
return ( 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"
<LLMItem type="button"
key={provider.name} onClick={() => setSearchMenuOpen(true)}
name={provider.name} >
value={provider.value} <div className="flex gap-x-4 items-center">
image={provider.logo} <img
description={provider.description} src={selectedProviderObject.logo}
checked={selectedProvider === provider.value} alt={`${selectedProviderObject.name} logo`}
onClick={() => updateProviderChoice(provider.value)} className="w-10 h-10 rounded-md"
/> />
); <div className="flex flex-col text-left">
})} <div className="text-sm font-semibold text-white">
</div> {selectedProviderObject.name}
</div> </div>
<div <div className="mt-1 text-xs text-[#D2D5DB]">
onChange={() => setHasChanges(true)} {selectedProviderObject.description}
className="mt-4 flex flex-col gap-y-1" </div>
> </div>
{selectedProvider && </div>
PROVIDERS.find( <CaretUpDown
(provider) => provider.value === selectedProvider size={24}
)?.options} weight="bold"
</div> 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>
</div> </div>
</form> </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 Sidebar from "@/components/SettingsSidebar";
import { isMobile } from "react-device-detect"; import { isMobile } from "react-device-detect";
import System from "@/models/system"; import System from "@/models/system";
@ -13,7 +13,7 @@ import ZillizLogo from "@/media/vectordbs/zilliz.png";
import AstraDBLogo from "@/media/vectordbs/astraDB.png"; import AstraDBLogo from "@/media/vectordbs/astraDB.png";
import PreLoader from "@/components/Preloader"; import PreLoader from "@/components/Preloader";
import ChangeWarningModal from "@/components/ChangeWarning"; 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 LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions";
import ChromaDBOptions from "@/components/VectorDBSelection/ChromaDBOptions"; import ChromaDBOptions from "@/components/VectorDBSelection/ChromaDBOptions";
import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions"; import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions";
@ -35,8 +35,55 @@ export default function GeneralVectorDatabase() {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [filteredVDBs, setFilteredVDBs] = useState([]); const [filteredVDBs, setFilteredVDBs] = useState([]);
const [selectedVDB, setSelectedVDB] = useState(null); const [selectedVDB, setSelectedVDB] = useState(null);
const [searchMenuOpen, setSearchMenuOpen] = useState(false);
const searchInputRef = useRef(null);
const { isOpen, openModal, closeModal } = useModal(); 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(() => { useEffect(() => {
async function fetchKeys() { async function fetchKeys() {
const _settings = await System.keys(); const _settings = await System.keys();
@ -48,6 +95,13 @@ export default function GeneralVectorDatabase() {
fetchKeys(); fetchKeys();
}, []); }, []);
useEffect(() => {
const filtered = VECTOR_DBS.filter((vdb) =>
vdb.name.toLowerCase().includes(searchQuery.toLowerCase())
);
setFilteredVDBs(filtered);
}, [searchQuery, selectedVDB]);
const VECTOR_DBS = [ const VECTOR_DBS = [
{ {
name: "LanceDB", name: "LanceDB",
@ -111,46 +165,7 @@ export default function GeneralVectorDatabase() {
}, },
]; ];
const updateVectorChoice = (selection) => { const selectedVDBObject = VECTOR_DBS.find((vdb) => vdb.value === selectedVDB);
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]);
return ( return (
<div className="w-screen h-screen overflow-hidden bg-sidebar flex"> <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="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="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"> <p className="text-lg leading-6 font-bold text-white">
Vector Database Vector Database
</p> </p>
@ -196,55 +211,94 @@ export default function GeneralVectorDatabase() {
are current and correct. are current and correct.
</p> </p>
</div> </div>
<div className="text-sm font-medium text-white mt-6 mb-4"> <div className="text-base font-bold text-white mt-6 mb-4">
Vector Database Providers Vector Database Provider
</div> </div>
<div className="w-full"> <div className="relative">
<div className="w-full relative border-slate-300/20 shadow border-4 rounded-xl text-white"> {searchMenuOpen && (
<div className="w-full p-4 absolute top-0 rounded-t-lg backdrop-blur-sm"> <div
<div className="w-full flex items-center sticky top-0"> className="fixed top-0 left-0 w-full h-full bg-black bg-opacity-70 backdrop-blur-sm z-10"
<MagnifyingGlass onClick={() => setSearchMenuOpen(false)}
size={16} />
weight="bold" )}
className="absolute left-4 z-30 text-white" {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">
<input <div className="w-full flex flex-col gap-y-1">
type="text" <div className="flex items-center sticky top-0 border-b border-[#9CA3AF] mx-4 bg-[#18181B]">
placeholder="Search vector databases" <MagnifyingGlass
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" size={20}
onChange={(e) => { weight="bold"
e.preventDefault(); className="absolute left-4 z-30 text-white -ml-4 my-2"
setSearchQuery(e.target.value); />
}} <input
autoComplete="off" type="text"
onKeyDown={(e) => { name="vdb-search"
if (e.key === "Enter") e.preventDefault(); 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> </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) => ( <button
<VectorDBItem 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"
key={vdb.name} type="button"
name={vdb.name} onClick={() => setSearchMenuOpen(true)}
value={vdb.value} >
image={vdb.logo} <div className="flex gap-x-4 items-center">
description={vdb.description} <img
checked={selectedVDB === vdb.value} src={selectedVDBObject.logo}
onClick={() => updateVectorChoice(vdb.value)} alt={`${selectedVDBObject.name} logo`}
className="w-10 h-10 rounded-md"
/> />
))} <div className="flex flex-col text-left">
</div> <div className="text-sm font-semibold text-white">
</div> {selectedVDBObject.name}
<div </div>
onChange={() => setHasChanges(true)} <div className="mt-1 text-xs text-[#D2D5DB]">
className="mt-4 flex flex-col gap-y-1" {selectedVDBObject.description}
> </div>
{selectedVDB && </div>
VECTOR_DBS.find((vdb) => vdb.value === selectedVDB) </div>
?.options} <CaretUpDown
</div> 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>
</div> </div>
</form> </form>