diff --git a/frontend/src/components/EmbeddingSelection/EmbedderItem/index.jsx b/frontend/src/components/EmbeddingSelection/EmbedderItem/index.jsx index e1f164a61526457580ea935573a77c64eae22fb7..72b3d73ef97b0ccf41ac55b25a1f98cb0c73b257 100644 --- a/frontend/src/components/EmbeddingSelection/EmbedderItem/index.jsx +++ b/frontend/src/components/EmbeddingSelection/EmbedderItem/index.jsx @@ -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> diff --git a/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx b/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx index 0b384c1f63b04d3e44468b9651c999f0571f2029..1cbfc689feb04a895903e562a18e7bb7cee9a909 100644 --- a/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/NativeEmbeddingOptions/index.jsx @@ -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. diff --git a/frontend/src/components/LLMSelection/LLMItem/index.jsx b/frontend/src/components/LLMSelection/LLMItem/index.jsx index 5e37738c550be9f88057474495ae68400c6e163b..e6b643a499420baf130428151b88f86cd8f1761a 100644 --- a/frontend/src/components/LLMSelection/LLMItem/index.jsx +++ b/frontend/src/components/LLMSelection/LLMItem/index.jsx @@ -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> diff --git a/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx index 942a3666da7b78aac975a45e53fc4099b836e724..e78f571a0bfcca7ff76a9d786faad040676ba79f 100644 --- a/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx +++ b/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx @@ -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> diff --git a/frontend/src/components/VectorDBSelection/VectorDBItem/index.jsx b/frontend/src/components/VectorDBSelection/VectorDBItem/index.jsx index ec35537bf60e3cdf793d70263cff856522a269dd..04a61b4017e9fe495ad79ca421a47dae6261c94c 100644 --- a/frontend/src/components/VectorDBSelection/VectorDBItem/index.jsx +++ b/frontend/src/components/VectorDBSelection/VectorDBItem/index.jsx @@ -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> diff --git a/frontend/src/index.css b/frontend/src/index.css index c94ca23b937e5381867cd506c8fc6882afb08a12..9b69ac9cf142653db4bee04a2b942d915e71572c 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -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; +} diff --git a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx index 77204c3cb73b2d2b8a0337074229381052d39ffe..d2b265560ded56b8eadb951aea007937d76024ae 100644 --- a/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/EmbeddingPreference/index.jsx @@ -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> - </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 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> - <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> diff --git a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx index c0a2a600bb199fdbd5c90b05d422ab5ff8d5a8e1..b9525c925610bcc920d42fb2c3de3160212b04cf 100644 --- a/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/LLMPreference/index.jsx @@ -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> - </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 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> - <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> diff --git a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx index a56dc26e7c922333c76d61d98122739a5181252a..c4d20ef4ba9db035ef598f8cb7a2b392fd384ef1 100644 --- a/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx +++ b/frontend/src/pages/GeneralSettings/TranscriptionPreference/index.jsx @@ -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> - </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 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> - <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> diff --git a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx index 0b2225cb325467570fe4e4517079e4f377df7338..df0307d1136583b190a3241726e57742b87540da 100644 --- a/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx +++ b/frontend/src/pages/GeneralSettings/VectorDatabase/index.jsx @@ -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>