mirror of
https://github.com/Mintplex-Labs/anything-llm.git
synced 2024-10-02 08:50:11 +02:00
Onboarding V2 (#502)
* WIP onboarding v2 * Welcome screen for onboarding complete * fix home page and WIP create skeleton for llm preference search/options * render llms as options * add search functionality to llm preference & add survey step * fix openai settings undefined & create custom logo onboarding page * add user setup UI * add data handling & privacy onboarding screen * add create workspace onboarding screen * fix survey width in onboarding * create vector database connection onboarding page * add workspace image & all skeleton ui complete * fix navigation buttons and ui tweaks to fit on screen * WIP LLMPreference * LLM Preference screen fully functional * create components for vector db options and fix styling of azure options * remove unneeded comment * vector db connection onboarding screen complete * minor ui tweak to searchbar * user setup page fully working * create workspace onboarding page fully working * useNavigate for navigation between pages * mobile layout, cleanup old files, survey functionality implemented * fix default logo appearing when should be blank & password setup bug fix * Modify flow of onboarding todo: embedding set up * Add embedder setup screen & insert into flow * update embedding back button auto-dismiss toasts on each step * move page defs under imports fix bg color on mobile styling --------- Co-authored-by: shatfield4 <seanhatfield5@gmail.com>
This commit is contained in:
parent
92da23e963
commit
d8ca92df88
@ -120,6 +120,7 @@ export default function App() {
|
|||||||
|
|
||||||
{/* Onboarding Flow */}
|
{/* Onboarding Flow */}
|
||||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||||
|
<Route path="/onboarding/:step" element={<OnboardingFlow />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
<ToastContainer />
|
<ToastContainer />
|
||||||
</PfpProvider>
|
</PfpProvider>
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export default function AzureAiOptions({ settings }) {
|
export default function AzureAiOptions({ settings }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
Azure Service Endpoint
|
Azure Service Endpoint
|
||||||
@ -48,6 +49,7 @@ export default function AzureAiOptions({ settings }) {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -10,7 +10,8 @@ export default function LocalAiOptions({ settings }) {
|
|||||||
const [apiKey, setApiKey] = useState(settings?.LocalAiApiKey);
|
const [apiKey, setApiKey] = useState(settings?.LocalAiApiKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
@ -54,12 +55,10 @@ export default function LocalAiOptions({ settings }) {
|
|||||||
<div className="w-full flex items-center gap-4">
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<div className="flex flex-col gap-y-1 mb-4">
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
<label className="text-white text-sm font-semibold block">
|
<label className="text-white text-sm font-semibold flex items-center gap-x-2">
|
||||||
Local AI API Key
|
Local AI API Key{" "}
|
||||||
|
<p className="!text-xs !italic !font-thin">optional</p>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-xs italic text-white/60">
|
|
||||||
optional API key to use if running LocalAI with API keys.
|
|
||||||
</p>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<input
|
<input
|
||||||
@ -75,7 +74,8 @@ export default function LocalAiOptions({ settings }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export default function OpenAiOptions({ settings }) {
|
export default function OpenAiOptions({ settings }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
API Key
|
API Key
|
||||||
@ -29,6 +30,7 @@ export default function OpenAiOptions({ settings }) {
|
|||||||
</option>
|
</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
export default function AzureAiOptions({ settings }) {
|
export default function AzureAiOptions({ settings }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
Azure Service Endpoint
|
Azure Service Endpoint
|
||||||
@ -48,7 +49,9 @@ export default function AzureAiOptions({ settings }) {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
Chat Model Token Limit
|
Chat Model Token Limit
|
||||||
@ -82,6 +85,8 @@ export default function AzureAiOptions({ settings }) {
|
|||||||
spellCheck={false}
|
spellCheck={false}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
<div className="flex-flex-col w-60"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,7 @@ export default function OpenAiOptions({ settings }) {
|
|||||||
const [openAIKey, setOpenAIKey] = useState(settings?.OpenAiKey);
|
const [openAIKey, setOpenAIKey] = useState(settings?.OpenAiKey);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="flex gap-x-4">
|
||||||
<div className="flex flex-col w-60">
|
<div className="flex flex-col w-60">
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
API Key
|
API Key
|
||||||
@ -25,7 +25,7 @@ export default function OpenAiOptions({ settings }) {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<OpenAIModelSelection settings={settings} apiKey={openAIKey} />
|
<OpenAIModelSelection settings={settings} apiKey={openAIKey} />
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ function OpenAIModelSelection({ apiKey, settings }) {
|
|||||||
<option
|
<option
|
||||||
key={model}
|
key={model}
|
||||||
value={model}
|
value={model}
|
||||||
selected={settings.OpenAiModelPref === model}
|
selected={settings?.OpenAiModelPref === model}
|
||||||
>
|
>
|
||||||
{model}
|
{model}
|
||||||
</option>
|
</option>
|
||||||
@ -102,7 +102,7 @@ function OpenAIModelSelection({ apiKey, settings }) {
|
|||||||
<option
|
<option
|
||||||
key={model.id}
|
key={model.id}
|
||||||
value={model.id}
|
value={model.id}
|
||||||
selected={settings.OpenAiModelPref === model.id}
|
selected={settings?.OpenAiModelPref === model.id}
|
||||||
>
|
>
|
||||||
{model.id}
|
{model.id}
|
||||||
</option>
|
</option>
|
||||||
|
@ -89,7 +89,7 @@ export function AdminRoute({ Component }) {
|
|||||||
if (isAuthd === null) return <FullScreenLoader />;
|
if (isAuthd === null) return <FullScreenLoader />;
|
||||||
|
|
||||||
if (shouldRedirectToOnboarding) {
|
if (shouldRedirectToOnboarding) {
|
||||||
return <Navigate to={paths.onboarding()} />;
|
return <Navigate to={paths.onboarding.home()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userFromStorage();
|
const user = userFromStorage();
|
||||||
@ -110,7 +110,7 @@ export function ManagerRoute({ Component }) {
|
|||||||
if (isAuthd === null) return <FullScreenLoader />;
|
if (isAuthd === null) return <FullScreenLoader />;
|
||||||
|
|
||||||
if (shouldRedirectToOnboarding) {
|
if (shouldRedirectToOnboarding) {
|
||||||
return <Navigate to={paths.onboarding()} />;
|
return <Navigate to={paths.onboarding.home()} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = userFromStorage();
|
const user = userFromStorage();
|
||||||
|
@ -0,0 +1,51 @@
|
|||||||
|
export default function ChromaDBOptions({ settings }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Chroma Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="ChromaEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:8000"
|
||||||
|
defaultValue={settings?.ChromaEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Header
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="ChromaApiHeader"
|
||||||
|
autoComplete="off"
|
||||||
|
type="text"
|
||||||
|
defaultValue={settings?.ChromaApiHeader}
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="X-Api-Key"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="ChromaApiKey"
|
||||||
|
autoComplete="off"
|
||||||
|
type="password"
|
||||||
|
defaultValue={settings?.ChromaApiKey ? "*".repeat(20) : ""}
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="sk-myApiKeyToAccessMyChromaInstance"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,9 @@
|
|||||||
|
export default function LanceDBOptions() {
|
||||||
|
return (
|
||||||
|
<div className="w-full h-10 items-center justify-center flex">
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
There is no configuration needed for LanceDB.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,55 @@
|
|||||||
|
export default function PineconeDBOptions({ settings }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone DB API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="PineConeKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="Pinecone API Key"
|
||||||
|
defaultValue={settings?.PineConeKey ? "*".repeat(20) : ""}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone Index Environment
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="PineConeEnvironment"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="us-gcp-west-1"
|
||||||
|
defaultValue={settings?.PineConeEnvironment}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Pinecone Index Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="PineConeIndex"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="my-index"
|
||||||
|
defaultValue={settings?.PineConeIndex}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
export default function QDrantDBOptions({ settings }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
QDrant API Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="QdrantEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:6633"
|
||||||
|
defaultValue={settings?.QdrantEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="QdrantApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="wOeqxsYP4....1244sba"
|
||||||
|
defaultValue={settings?.QdrantApiKey}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,38 @@
|
|||||||
|
export default function WeaviateDBOptions({ settings }) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
Weaviate Endpoint
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="WeaviateEndpoint"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="http://localhost:8080"
|
||||||
|
defaultValue={settings?.WeaviateEndpoint}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<label className="text-white text-sm font-semibold block mb-4">
|
||||||
|
API Key
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
type="password"
|
||||||
|
name="WeaviateApiKey"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="sk-123Abcweaviate"
|
||||||
|
defaultValue={settings?.WeaviateApiKey}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
BIN
frontend/src/media/illustrations/create-workspace.png
Normal file
BIN
frontend/src/media/illustrations/create-workspace.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 18 KiB |
@ -1,145 +0,0 @@
|
|||||||
import React, { memo, useEffect, useState } from "react";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import AnythingLLM from "@/media/logo/anything-llm.png";
|
|
||||||
import useLogo from "@/hooks/useLogo";
|
|
||||||
import { Plus } from "@phosphor-icons/react";
|
|
||||||
import showToast from "@/utils/toast";
|
|
||||||
|
|
||||||
function AppearanceSetup({ prevStep, nextStep }) {
|
|
||||||
const { logo: _initLogo, setLogo: _setLogo } = useLogo();
|
|
||||||
const [logo, setLogo] = useState("");
|
|
||||||
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function logoInit() {
|
|
||||||
setLogo(_initLogo || "");
|
|
||||||
const _isDefaultLogo = await System.isDefaultLogo();
|
|
||||||
setIsDefaultLogo(_isDefaultLogo);
|
|
||||||
}
|
|
||||||
logoInit();
|
|
||||||
}, [_initLogo]);
|
|
||||||
|
|
||||||
const handleFileUpload = async (event) => {
|
|
||||||
const file = event.target.files[0];
|
|
||||||
if (!file) return false;
|
|
||||||
|
|
||||||
const objectURL = URL.createObjectURL(file);
|
|
||||||
setLogo(objectURL);
|
|
||||||
|
|
||||||
const formData = new FormData();
|
|
||||||
formData.append("logo", file);
|
|
||||||
const { success, error } = await System.uploadLogo(formData);
|
|
||||||
if (!success) {
|
|
||||||
showToast(`Failed to upload logo: ${error}`, "error");
|
|
||||||
setLogo(_initLogo);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoURL = await System.fetchLogo();
|
|
||||||
_setLogo(logoURL);
|
|
||||||
|
|
||||||
showToast("Image uploaded successfully.", "success");
|
|
||||||
setIsDefaultLogo(false);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRemoveLogo = async () => {
|
|
||||||
setLogo("");
|
|
||||||
setIsDefaultLogo(true);
|
|
||||||
|
|
||||||
const { success, error } = await System.removeCustomLogo();
|
|
||||||
if (!success) {
|
|
||||||
console.error("Failed to remove logo:", error);
|
|
||||||
showToast(`Failed to remove logo: ${error}`, "error");
|
|
||||||
const logoURL = await System.fetchLogo();
|
|
||||||
setLogo(logoURL);
|
|
||||||
setIsDefaultLogo(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const logoURL = await System.fetchLogo();
|
|
||||||
_setLogo(logoURL);
|
|
||||||
|
|
||||||
showToast("Image successfully removed.", "success");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex flex-col w-full px-8 py-4">
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<h2 className="text-white text-sm font-medium">Custom Logo</h2>
|
|
||||||
<p className="text-sm font-base text-white/60">
|
|
||||||
Upload your custom logo to make your chatbot yours.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<div className="flex md:flex-row flex-col items-center">
|
|
||||||
<img
|
|
||||||
src={logo}
|
|
||||||
alt="Uploaded Logo"
|
|
||||||
className="w-48 h-48 object-contain mr-6"
|
|
||||||
hidden={isDefaultLogo}
|
|
||||||
onError={(e) => (e.target.src = AnythingLLM)}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-row gap-x-8">
|
|
||||||
<label className="mt-5 hover:opacity-60" hidden={!isDefaultLogo}>
|
|
||||||
<input
|
|
||||||
id="logo-upload"
|
|
||||||
type="file"
|
|
||||||
accept="image/*"
|
|
||||||
className="hidden"
|
|
||||||
onChange={handleFileUpload}
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
className="w-80 py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex cursor-pointer"
|
|
||||||
htmlFor="logo-upload"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col items-center justify-center">
|
|
||||||
<div className="rounded-full bg-white/40">
|
|
||||||
<Plus className="w-6 h-6 text-black/80 m-2" />
|
|
||||||
</div>
|
|
||||||
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
|
||||||
Add a custom logo
|
|
||||||
</div>
|
|
||||||
<div className="text-white text-opacity-60 text-xs font-medium py-1">
|
|
||||||
Recommended size: 800 x 200
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</label>
|
|
||||||
<button
|
|
||||||
onClick={handleRemoveLogo}
|
|
||||||
className="text-white text-base font-medium hover:text-opacity-60"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-6 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={() => nextStep("user_mode_setup")}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => nextStep("user_mode_setup")}
|
|
||||||
type="button"
|
|
||||||
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default memo(AppearanceSetup);
|
|
@ -1,68 +0,0 @@
|
|||||||
import React, { memo } from "react";
|
|
||||||
import { useNavigate } from "react-router-dom";
|
|
||||||
import paths from "@/utils/paths";
|
|
||||||
import Workspace from "@/models/workspace";
|
|
||||||
|
|
||||||
function CreateFirstWorkspace({ prevStep }) {
|
|
||||||
const navigate = useNavigate();
|
|
||||||
|
|
||||||
const handleCreate = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = new FormData(e.target);
|
|
||||||
const { workspace, error } = await Workspace.new({
|
|
||||||
name: form.get("name"),
|
|
||||||
onboardingComplete: true,
|
|
||||||
});
|
|
||||||
if (!!workspace) {
|
|
||||||
navigate(paths.home());
|
|
||||||
} else {
|
|
||||||
alert(error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleCreate} className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col w-full md:px-8 py-12">
|
|
||||||
<div className="space-y-6 flex h-full w-96">
|
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block mb-2 text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Workspace name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="name"
|
|
||||||
type="text"
|
|
||||||
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="My workspace"
|
|
||||||
minLength={4}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-end items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Finish
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default memo(CreateFirstWorkspace);
|
|
@ -1,136 +0,0 @@
|
|||||||
import React, { memo, useEffect, useState } from "react";
|
|
||||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
|
||||||
import OpenAiLogo from "@/media/llmprovider/openai.png";
|
|
||||||
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
|
|
||||||
import LocalAiLogo from "@/media/llmprovider/localai.png";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import PreLoader from "@/components/Preloader";
|
|
||||||
import LLMProviderOption from "@/components/LLMSelection/LLMProviderOption";
|
|
||||||
import OpenAiOptions from "@/components/EmbeddingSelection/OpenAiOptions";
|
|
||||||
import AzureAiOptions from "@/components/EmbeddingSelection/AzureAiOptions";
|
|
||||||
import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
|
|
||||||
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
|
|
||||||
|
|
||||||
function EmbeddingSelection({ nextStep, prevStep, currentStep }) {
|
|
||||||
const [embeddingChoice, setEmbeddingChoice] = useState("native");
|
|
||||||
const [settings, setSettings] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
const updateChoice = (selection) => {
|
|
||||||
setEmbeddingChoice(selection);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchKeys() {
|
|
||||||
const _settings = await System.keys();
|
|
||||||
setSettings(_settings);
|
|
||||||
setEmbeddingChoice(_settings?.EmbeddingEngine || "native");
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
fetchKeys();
|
|
||||||
}, [currentStep]);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const data = {};
|
|
||||||
const formData = new FormData(form);
|
|
||||||
for (var [key, value] of formData.entries()) data[key] = value;
|
|
||||||
const { error } = await System.updateSystem(data);
|
|
||||||
if (error) {
|
|
||||||
alert(`Failed to save LLM settings: ${error}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nextStep("vector_database");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading)
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex justify-center items-center p-20">
|
|
||||||
<PreLoader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col w-full px-1 md:px-8 py-4">
|
|
||||||
<div className="text-white text-sm font-medium pb-4">
|
|
||||||
Embedding Provider
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[752px]">
|
|
||||||
<input
|
|
||||||
hidden={true}
|
|
||||||
name="EmbeddingEngine"
|
|
||||||
value={embeddingChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="AnythingLLM Embedder"
|
|
||||||
value="native"
|
|
||||||
description="Use the built-in embedding engine for AnythingLLM. Zero setup!"
|
|
||||||
checked={embeddingChoice === "native"}
|
|
||||||
image={AnythingLLMIcon}
|
|
||||||
onClick={updateChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="OpenAI"
|
|
||||||
value="openai"
|
|
||||||
link="openai.com"
|
|
||||||
description="The standard option for most non-commercial use."
|
|
||||||
checked={embeddingChoice === "openai"}
|
|
||||||
image={OpenAiLogo}
|
|
||||||
onClick={updateChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Azure OpenAI"
|
|
||||||
value="azure"
|
|
||||||
link="azure.microsoft.com"
|
|
||||||
description="The enterprise option of OpenAI hosted on Azure services."
|
|
||||||
checked={embeddingChoice === "azure"}
|
|
||||||
image={AzureOpenAiLogo}
|
|
||||||
onClick={updateChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="LocalAI"
|
|
||||||
value="localai"
|
|
||||||
link="localai.io"
|
|
||||||
description="Self hosted LocalAI embedding engine."
|
|
||||||
checked={embeddingChoice === "localai"}
|
|
||||||
image={LocalAiLogo}
|
|
||||||
onClick={updateChoice}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex flex-wrap gap-4 max-w-[752px]">
|
|
||||||
{embeddingChoice === "native" && <NativeEmbeddingOptions />}
|
|
||||||
{embeddingChoice === "openai" && (
|
|
||||||
<OpenAiOptions settings={settings} />
|
|
||||||
)}
|
|
||||||
{embeddingChoice === "azure" && (
|
|
||||||
<AzureAiOptions settings={settings} />
|
|
||||||
)}
|
|
||||||
{embeddingChoice === "localai" && (
|
|
||||||
<LocalAiOptions settings={settings} />
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(EmbeddingSelection);
|
|
@ -1,183 +0,0 @@
|
|||||||
import React, { memo, useEffect, useState } from "react";
|
|
||||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
|
||||||
import OpenAiLogo from "@/media/llmprovider/openai.png";
|
|
||||||
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
|
|
||||||
import AnthropicLogo from "@/media/llmprovider/anthropic.png";
|
|
||||||
import GeminiLogo from "@/media/llmprovider/gemini.png";
|
|
||||||
import OllamaLogo from "@/media/llmprovider/ollama.png";
|
|
||||||
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
|
|
||||||
import LocalAiLogo from "@/media/llmprovider/localai.png";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import PreLoader from "@/components/Preloader";
|
|
||||||
import LLMProviderOption from "@/components/LLMSelection/LLMProviderOption";
|
|
||||||
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
|
||||||
import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions";
|
|
||||||
import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions";
|
|
||||||
import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions";
|
|
||||||
import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions";
|
|
||||||
import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions";
|
|
||||||
import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions";
|
|
||||||
import OllamaLLMOptions from "@/components/LLMSelection/OllamaLLMOptions";
|
|
||||||
|
|
||||||
function LLMSelection({ nextStep, prevStep, currentStep }) {
|
|
||||||
const [llmChoice, setLLMChoice] = useState("openai");
|
|
||||||
const [settings, setSettings] = useState(null);
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
const updateLLMChoice = (selection) => {
|
|
||||||
setLLMChoice(selection);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchKeys() {
|
|
||||||
const _settings = await System.keys();
|
|
||||||
setSettings(_settings);
|
|
||||||
setLLMChoice(_settings?.LLMProvider || "openai");
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentStep === "llm_preference") {
|
|
||||||
fetchKeys();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const data = {};
|
|
||||||
const formData = new FormData(form);
|
|
||||||
for (var [key, value] of formData.entries()) data[key] = value;
|
|
||||||
const { error } = await System.updateSystem(data);
|
|
||||||
if (error) {
|
|
||||||
alert(`Failed to save LLM settings: ${error}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nextStep("embedding_preferences");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading)
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex justify-center items-center p-20">
|
|
||||||
<PreLoader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col w-full px-1 md:px-8 py-4">
|
|
||||||
<div className="text-white text-sm font-medium pb-4">
|
|
||||||
LLM Providers
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[752px]">
|
|
||||||
<input hidden={true} name="LLMProvider" value={llmChoice} />
|
|
||||||
<LLMProviderOption
|
|
||||||
name="OpenAI"
|
|
||||||
value="openai"
|
|
||||||
link="openai.com"
|
|
||||||
description="The standard option for most non-commercial use."
|
|
||||||
checked={llmChoice === "openai"}
|
|
||||||
image={OpenAiLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Azure OpenAI"
|
|
||||||
value="azure"
|
|
||||||
link="azure.microsoft.com"
|
|
||||||
description="The enterprise option of OpenAI hosted on Azure services."
|
|
||||||
checked={llmChoice === "azure"}
|
|
||||||
image={AzureOpenAiLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Anthropic Claude 2"
|
|
||||||
value="anthropic"
|
|
||||||
link="anthropic.com"
|
|
||||||
description="A friendly AI Assistant hosted by Anthropic. Provides chat services only!"
|
|
||||||
checked={llmChoice === "anthropic"}
|
|
||||||
image={AnthropicLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Google Gemini"
|
|
||||||
value="gemini"
|
|
||||||
link="ai.google.dev"
|
|
||||||
description="Google's largest and most capable AI model"
|
|
||||||
checked={llmChoice === "gemini"}
|
|
||||||
image={GeminiLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="LM Studio"
|
|
||||||
value="lmstudio"
|
|
||||||
link="lmstudio.ai"
|
|
||||||
description="Discover, download, and run thousands of cutting edge LLMs in a few clicks."
|
|
||||||
checked={llmChoice === "lmstudio"}
|
|
||||||
image={LMStudioLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Local AI"
|
|
||||||
value="localai"
|
|
||||||
link="localai.io"
|
|
||||||
description="Run LLMs locally on your own machine."
|
|
||||||
checked={llmChoice === "localai"}
|
|
||||||
image={LocalAiLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Ollama"
|
|
||||||
value="ollama"
|
|
||||||
link="ollama.ai"
|
|
||||||
description="Run LLMs locally on your own machine."
|
|
||||||
checked={llmChoice === "ollama"}
|
|
||||||
image={OllamaLogo}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
{!window.location.hostname.includes("useanything.com") && (
|
|
||||||
<LLMProviderOption
|
|
||||||
name="Custom Llama Model"
|
|
||||||
value="native"
|
|
||||||
description="Use a downloaded custom Llama model for chatting on this AnythingLLM instance."
|
|
||||||
checked={llmChoice === "native"}
|
|
||||||
image={AnythingLLMIcon}
|
|
||||||
onClick={updateLLMChoice}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex flex-wrap gap-4 max-w-[752px]">
|
|
||||||
{llmChoice === "openai" && <OpenAiOptions settings={settings} />}
|
|
||||||
{llmChoice === "azure" && <AzureAiOptions settings={settings} />}
|
|
||||||
{llmChoice === "anthropic" && (
|
|
||||||
<AnthropicAiOptions settings={settings} />
|
|
||||||
)}
|
|
||||||
{llmChoice === "gemini" && <GeminiLLMOptions settings={settings} />}
|
|
||||||
{llmChoice === "lmstudio" && (
|
|
||||||
<LMStudioOptions settings={settings} />
|
|
||||||
)}
|
|
||||||
{llmChoice === "localai" && <LocalAiOptions settings={settings} />}
|
|
||||||
{llmChoice === "ollama" && <OllamaLLMOptions settings={settings} />}
|
|
||||||
{llmChoice === "native" && <NativeLLMOptions settings={settings} />}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(LLMSelection);
|
|
@ -1,117 +0,0 @@
|
|||||||
import React, { useState, memo } from "react";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
|
|
||||||
import debounce from "lodash.debounce";
|
|
||||||
|
|
||||||
// Multi-user mode step
|
|
||||||
function MultiUserSetup({ nextStep, prevStep }) {
|
|
||||||
const [username, setUsername] = useState("");
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const data = {
|
|
||||||
username: formData.get("username"),
|
|
||||||
password: formData.get("password"),
|
|
||||||
};
|
|
||||||
const { success, error } = await System.setupMultiUser(data);
|
|
||||||
if (!success) {
|
|
||||||
alert(error);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-request token with credentials that was just set so they
|
|
||||||
// are not redirected to login after completion.
|
|
||||||
const { user, token } = await System.requestToken(data);
|
|
||||||
window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
|
|
||||||
window.localStorage.setItem(AUTH_TOKEN, token);
|
|
||||||
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
|
||||||
|
|
||||||
nextStep("data_handling");
|
|
||||||
};
|
|
||||||
|
|
||||||
const setNewUsername = (e) => setUsername(e.target.value);
|
|
||||||
const setNewPassword = (e) => setPassword(e.target.value);
|
|
||||||
const handleUsernameChange = debounce(setNewUsername, 500);
|
|
||||||
const handlePasswordChange = debounce(setNewPassword, 500);
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit}>
|
|
||||||
<div className="flex flex-col w-full md:px-8 py-4">
|
|
||||||
<div className="space-y-6 flex h-full w-96">
|
|
||||||
<div className="w-full flex flex-col gap-y-4">
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block mb-2 text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Admin account username
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="username"
|
|
||||||
type="text"
|
|
||||||
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="Your admin username"
|
|
||||||
minLength={6}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={handleUsernameChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label
|
|
||||||
htmlFor="name"
|
|
||||||
className="block mb-2 text-sm font-medium text-white"
|
|
||||||
>
|
|
||||||
Admin account password
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="password"
|
|
||||||
type="password"
|
|
||||||
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="Your admin password"
|
|
||||||
minLength={8}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<p className="w-96 text-white text-opacity-80 text-xs font-base">
|
|
||||||
Username must be at least 6 characters long. Password must be at
|
|
||||||
least 8 characters long.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-6 border-t rounded-b border-gray-500/50">
|
|
||||||
<div className="w-96 text-white text-opacity-80 text-xs font-base">
|
|
||||||
By default, you will be the only admin. As an admin you will need to
|
|
||||||
create accounts for all new users or admins. Do not lose your
|
|
||||||
password as only admins can reset passwords.
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border px-4 py-2 rounded-lg text-sm items-center flex gap-x-2
|
|
||||||
border-slate-200 text-slate-800 bg-slate-200 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow
|
|
||||||
disabled:border-gray-400 disabled:text-slate-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
||||||
disabled={!(!!username && !!password)}
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default memo(MultiUserSetup);
|
|
@ -1,103 +0,0 @@
|
|||||||
import React, { memo, useState } from "react";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
|
|
||||||
import debounce from "lodash.debounce";
|
|
||||||
|
|
||||||
function PasswordProtection({ nextStep, prevStep }) {
|
|
||||||
const [password, setPassword] = useState("");
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const { error } = await System.updateSystemPassword({
|
|
||||||
usePassword: true,
|
|
||||||
newPassword: formData.get("password"),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
alert(`Failed to set password: ${error}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Auto-request token with password that was just set so they
|
|
||||||
// are not redirected to login after completion.
|
|
||||||
const { token } = await System.requestToken({
|
|
||||||
password: formData.get("password"),
|
|
||||||
});
|
|
||||||
window.localStorage.removeItem(AUTH_USER);
|
|
||||||
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
|
||||||
window.localStorage.setItem(AUTH_TOKEN, token);
|
|
||||||
|
|
||||||
nextStep("data_handling");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
|
||||||
nextStep("data_handling");
|
|
||||||
};
|
|
||||||
|
|
||||||
const setNewPassword = (e) => setPassword(e.target.value);
|
|
||||||
const handlePasswordChange = debounce(setNewPassword, 500);
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<form className="flex flex-col w-full" onSubmit={handleSubmit}>
|
|
||||||
<div className="flex flex-col w-full px-1 md:px-8 py-4">
|
|
||||||
<div className="w-full flex flex-col gap-y-2 my-5">
|
|
||||||
<div className="w-80">
|
|
||||||
<div className="flex flex-col mb-3 ">
|
|
||||||
<label
|
|
||||||
htmlFor="password"
|
|
||||||
className="block font-medium text-white"
|
|
||||||
>
|
|
||||||
New Password
|
|
||||||
</label>
|
|
||||||
<p className="text-slate-300 text-xs">
|
|
||||||
must be at least 8 characters.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
onChange={handlePasswordChange}
|
|
||||||
name="password"
|
|
||||||
type="text"
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
|
|
||||||
placeholder="Your Instance Password"
|
|
||||||
minLength={8}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleSkip}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
disabled={!password}
|
|
||||||
className="border px-4 py-2 rounded-lg text-sm items-center flex gap-x-2
|
|
||||||
border-slate-200 text-slate-800 bg-slate-200 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow
|
|
||||||
disabled:border-gray-400 disabled:text-slate-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default memo(PasswordProtection);
|
|
@ -1,47 +0,0 @@
|
|||||||
import React, { memo } from "react";
|
|
||||||
|
|
||||||
// How many people will be using your instance step
|
|
||||||
function UserModeSelection({ nextStep, prevStep }) {
|
|
||||||
const justMeClicked = () => {
|
|
||||||
nextStep("password_protection");
|
|
||||||
};
|
|
||||||
|
|
||||||
const myTeamClicked = () => {
|
|
||||||
nextStep("multi_user_mode");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div className="flex flex-col justify-center items-center px-20 py-14">
|
|
||||||
<div className="w-80 text-white text-center text-2xl font-base">
|
|
||||||
How many people will be using your instance?
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-4 justify-center my-8">
|
|
||||||
<button
|
|
||||||
onClick={justMeClicked}
|
|
||||||
className="transition-all duration-200 border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Just Me
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={myTeamClicked}
|
|
||||||
className="transition-all duration-200 border border-slate-200 px-5 py-2.5 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
My Team
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center p-6 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar transition-all duration-300"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(UserModeSelection);
|
|
@ -1,240 +0,0 @@
|
|||||||
import { COMPLETE_QUESTIONNAIRE } from "@/utils/constants";
|
|
||||||
import paths from "@/utils/paths";
|
|
||||||
import { CheckCircle, Circle } from "@phosphor-icons/react";
|
|
||||||
import React, { memo } from "react";
|
|
||||||
|
|
||||||
async function sendQuestionnaire({ email, useCase, comment }) {
|
|
||||||
if (import.meta.env.DEV) return;
|
|
||||||
return fetch(`https://onboarding-wxich7363q-uc.a.run.app`, {
|
|
||||||
method: "POST",
|
|
||||||
body: JSON.stringify({
|
|
||||||
email,
|
|
||||||
useCase,
|
|
||||||
comment,
|
|
||||||
sourceId: "0VRjqHh6Vukqi0x0Vd0n/m8JuT7k8nOz",
|
|
||||||
}),
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
window.localStorage.setItem(COMPLETE_QUESTIONNAIRE, true);
|
|
||||||
console.log(`✅ Questionnaire responses sent.`);
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.error(`sendQuestionnaire`, error.message);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function UserQuestionnaire({ nextStep, prevStep }) {
|
|
||||||
const handleSubmit = async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = e.target;
|
|
||||||
const formData = new FormData(form);
|
|
||||||
nextStep("create_workspace");
|
|
||||||
|
|
||||||
await sendQuestionnaire({
|
|
||||||
email: formData.get("email"),
|
|
||||||
useCase: formData.get("use_case") || "other",
|
|
||||||
comment: formData.get("comment") || null,
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSkip = () => {
|
|
||||||
nextStep("create_workspace");
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="w-full flex items-center justify-center px-1 md:px-8 py-4">
|
|
||||||
<div className="w-auto flex flex-col gap-y-1 items-center">
|
|
||||||
<CheckCircle size={60} className="text-green-500" />
|
|
||||||
<p className="text-zinc-300">Thank you for your feedback!</p>
|
|
||||||
<a
|
|
||||||
href={paths.mailToMintplex()}
|
|
||||||
className="text-blue-400 underline text-xs"
|
|
||||||
>
|
|
||||||
team@mintplexlabs.com
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleSkip}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border px-4 py-2 rounded-lg text-sm items-center flex gap-x-2
|
|
||||||
border-slate-200 text-slate-800 bg-slate-200 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow
|
|
||||||
disabled:border-gray-400 disabled:text-slate-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="w-full">
|
|
||||||
<form className="flex flex-col w-full" onSubmit={handleSubmit}>
|
|
||||||
<div className="flex flex-col w-full px-1 md:px-8 py-4">
|
|
||||||
<div className="w-full flex flex-col gap-y-2 my-5">
|
|
||||||
<div className="w-80">
|
|
||||||
<div className="flex flex-col mb-3 ">
|
|
||||||
<label htmlFor="email" className="block font-medium text-white">
|
|
||||||
What is your email?
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<input
|
|
||||||
name="email"
|
|
||||||
type="email"
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:border-blue-500 block w-full p-2.5 placeholder-white placeholder-opacity-60 focus:ring-blue-500"
|
|
||||||
placeholder="you@gmail.com"
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-y-2 my-5">
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex flex-col mb-3 ">
|
|
||||||
<label
|
|
||||||
htmlFor="use_case"
|
|
||||||
className="block font-medium text-white"
|
|
||||||
>
|
|
||||||
How are you planning to use AnythingLLM?
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col gap-y-2">
|
|
||||||
<div class="flex items-center ps-4 border border-zinc-400 rounded group radio-container hover:bg-blue-400/10">
|
|
||||||
<input
|
|
||||||
id="bordered-radio-1"
|
|
||||||
type="radio"
|
|
||||||
value="business"
|
|
||||||
name="use_case"
|
|
||||||
class="sr-only peer"
|
|
||||||
/>
|
|
||||||
<Circle
|
|
||||||
weight="fill"
|
|
||||||
className="fill-transparent border border-gray-300 rounded-full peer-checked:fill-blue-500 peer-checked:border-none"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="bordered-radio-1"
|
|
||||||
class="w-full py-4 ms-2 text-sm font-medium text-gray-300"
|
|
||||||
>
|
|
||||||
For my business
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center ps-4 border border-zinc-400 rounded group radio-container hover:bg-blue-400/10">
|
|
||||||
<input
|
|
||||||
id="bordered-radio-2"
|
|
||||||
type="radio"
|
|
||||||
value="personal"
|
|
||||||
name="use_case"
|
|
||||||
class="sr-only peer"
|
|
||||||
/>
|
|
||||||
<Circle
|
|
||||||
weight="fill"
|
|
||||||
className="fill-transparent border border-gray-300 rounded-full peer-checked:fill-blue-500 peer-checked:border-none"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="bordered-radio-2"
|
|
||||||
class="w-full py-4 ms-2 text-sm font-medium text-gray-300"
|
|
||||||
>
|
|
||||||
For personal use
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center ps-4 border border-zinc-400 rounded group radio-container hover:bg-blue-400/10">
|
|
||||||
<input
|
|
||||||
id="bordered-radio-3"
|
|
||||||
type="radio"
|
|
||||||
value="other"
|
|
||||||
name="use_case"
|
|
||||||
class="sr-only peer"
|
|
||||||
/>
|
|
||||||
<Circle
|
|
||||||
weight="fill"
|
|
||||||
className="fill-transparent border border-gray-300 rounded-full peer-checked:fill-blue-500 peer-checked:border-none"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
for="bordered-radio-3"
|
|
||||||
class="w-full py-4 ms-2 text-sm font-medium text-gray-300"
|
|
||||||
>
|
|
||||||
I'm not sure yet
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="w-full flex flex-col gap-y-2 my-5">
|
|
||||||
<div className="w-full">
|
|
||||||
<div className="flex flex-col mb-3 ">
|
|
||||||
<label
|
|
||||||
htmlFor="comments"
|
|
||||||
className="block font-medium text-white"
|
|
||||||
>
|
|
||||||
Any comments for the team?
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
<textarea
|
|
||||||
name="comment"
|
|
||||||
rows={5}
|
|
||||||
className="bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
|
||||||
placeholder="If you have any questions or comments right now, you can leave them here and we will get back to you. You can also email team@mintplexlabs.com"
|
|
||||||
wrap="soft"
|
|
||||||
autoComplete="off"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<div className="flex gap-2">
|
|
||||||
<button
|
|
||||||
onClick={handleSkip}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Skip
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border px-4 py-2 rounded-lg text-sm items-center flex gap-x-2
|
|
||||||
border-slate-200 text-slate-800 bg-slate-200 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow
|
|
||||||
disabled:border-gray-400 disabled:text-slate-800 disabled:bg-gray-400 disabled:cursor-not-allowed"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
export default memo(UserQuestionnaire);
|
|
@ -1,310 +0,0 @@
|
|||||||
import React, { memo, useEffect, useState } from "react";
|
|
||||||
|
|
||||||
import VectorDBOption from "@/components/VectorDBOption";
|
|
||||||
import ChromaLogo from "@/media/vectordbs/chroma.png";
|
|
||||||
import PineconeLogo from "@/media/vectordbs/pinecone.png";
|
|
||||||
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
|
|
||||||
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
|
|
||||||
import QDrantLogo from "@/media/vectordbs/qdrant.png";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import PreLoader from "@/components/Preloader";
|
|
||||||
|
|
||||||
function VectorDatabaseConnection({ nextStep, prevStep, currentStep }) {
|
|
||||||
const [vectorDB, setVectorDB] = useState("lancedb");
|
|
||||||
const [settings, setSettings] = useState({});
|
|
||||||
const [loading, setLoading] = useState(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
async function fetchKeys() {
|
|
||||||
const _settings = await System.keys();
|
|
||||||
setSettings(_settings);
|
|
||||||
setVectorDB(_settings?.VectorDB || "lancedb");
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
if (currentStep === "vector_database") {
|
|
||||||
fetchKeys();
|
|
||||||
}
|
|
||||||
}, [currentStep]);
|
|
||||||
|
|
||||||
const updateVectorChoice = (selection) => {
|
|
||||||
setVectorDB(selection);
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = async (e, formElement) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const form = formElement || e.target;
|
|
||||||
const data = {};
|
|
||||||
const formData = new FormData(form);
|
|
||||||
for (var [key, value] of formData.entries()) data[key] = value;
|
|
||||||
const { error } = await System.updateSystem(data);
|
|
||||||
if (error) {
|
|
||||||
alert(`Failed to save settings: ${error}`, "error");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
nextStep("appearance");
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
if (loading)
|
|
||||||
return (
|
|
||||||
<div className="w-full h-full flex justify-center items-center p-20">
|
|
||||||
<PreLoader />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<form onSubmit={handleSubmit} className="flex flex-col w-full">
|
|
||||||
<div className="flex flex-col w-full px-1 md:px-8 py-4">
|
|
||||||
<div className="text-white text-sm font-medium pb-4">
|
|
||||||
Select your preferred vector database provider
|
|
||||||
</div>
|
|
||||||
<div className="w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-[752px]">
|
|
||||||
<input hidden={true} name="VectorDB" value={vectorDB} />
|
|
||||||
<VectorDBOption
|
|
||||||
name="Chroma"
|
|
||||||
value="chroma"
|
|
||||||
link="trychroma.com"
|
|
||||||
description="Open source vector database you can host yourself or on the cloud."
|
|
||||||
checked={vectorDB === "chroma"}
|
|
||||||
image={ChromaLogo}
|
|
||||||
onClick={updateVectorChoice}
|
|
||||||
/>
|
|
||||||
<VectorDBOption
|
|
||||||
name="Pinecone"
|
|
||||||
value="pinecone"
|
|
||||||
link="pinecone.io"
|
|
||||||
description="100% cloud-based vector database for enterprise use cases."
|
|
||||||
checked={vectorDB === "pinecone"}
|
|
||||||
image={PineconeLogo}
|
|
||||||
onClick={updateVectorChoice}
|
|
||||||
/>
|
|
||||||
<VectorDBOption
|
|
||||||
name="QDrant"
|
|
||||||
value="qdrant"
|
|
||||||
link="qdrant.tech"
|
|
||||||
description="Open source local and distributed cloud vector database."
|
|
||||||
checked={vectorDB === "qdrant"}
|
|
||||||
image={QDrantLogo}
|
|
||||||
onClick={updateVectorChoice}
|
|
||||||
/>
|
|
||||||
<VectorDBOption
|
|
||||||
name="Weaviate"
|
|
||||||
value="weaviate"
|
|
||||||
link="weaviate.io"
|
|
||||||
description="Open source local and cloud hosted multi-modal vector database."
|
|
||||||
checked={vectorDB === "weaviate"}
|
|
||||||
image={WeaviateLogo}
|
|
||||||
onClick={updateVectorChoice}
|
|
||||||
/>
|
|
||||||
<VectorDBOption
|
|
||||||
name="LanceDB"
|
|
||||||
value="lancedb"
|
|
||||||
link="lancedb.com"
|
|
||||||
description="100% local vector DB that runs on the same instance as AnythingLLM."
|
|
||||||
checked={vectorDB === "lancedb"}
|
|
||||||
image={LanceDbLogo}
|
|
||||||
onClick={updateVectorChoice}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex flex-wrap gap-4 max-w-[752px]">
|
|
||||||
{vectorDB === "pinecone" && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
Pinecone DB API Key
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="PineConeKey"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="Pinecone API Key"
|
|
||||||
defaultValue={settings?.PineConeKey ? "*".repeat(20) : ""}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
Pinecone Index Environment
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="PineConeEnvironment"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="us-gcp-west-1"
|
|
||||||
defaultValue={settings?.PineConeEnvironment}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
Pinecone Index Name
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
name="PineConeIndex"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="my-index"
|
|
||||||
defaultValue={settings?.PineConeIndex}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{vectorDB === "chroma" && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
Chroma Endpoint
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
name="ChromaEndpoint"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="http://localhost:8000"
|
|
||||||
defaultValue={settings?.ChromaEndpoint}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
API Header
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="ChromaApiHeader"
|
|
||||||
autoComplete="off"
|
|
||||||
type="text"
|
|
||||||
defaultValue={settings?.ChromaApiHeader}
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="X-Api-Key"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
API Key
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
name="ChromaApiKey"
|
|
||||||
autoComplete="off"
|
|
||||||
type="password"
|
|
||||||
defaultValue={settings?.ChromaApiKey ? "*".repeat(20) : ""}
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="sk-myApiKeyToAccessMyChromaInstance"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{vectorDB === "lancedb" && (
|
|
||||||
<div className="w-full h-10 items-center justify-center flex">
|
|
||||||
<p className="text-sm font-base text-white text-opacity-60">
|
|
||||||
There is no configuration needed for LanceDB.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{vectorDB === "qdrant" && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
QDrant API Endpoint
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
name="QdrantEndpoint"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="http://localhost:6633"
|
|
||||||
defaultValue={settings?.QdrantEndpoint}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
API Key
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="QdrantApiKey"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="wOeqxsYP4....1244sba"
|
|
||||||
defaultValue={settings?.QdrantApiKey}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{vectorDB === "weaviate" && (
|
|
||||||
<>
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
Weaviate Endpoint
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="url"
|
|
||||||
name="WeaviateEndpoint"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="http://localhost:8080"
|
|
||||||
defaultValue={settings?.WeaviateEndpoint}
|
|
||||||
required={true}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className="flex flex-col w-60">
|
|
||||||
<label className="text-white text-sm font-semibold block mb-4">
|
|
||||||
API Key
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
type="password"
|
|
||||||
name="WeaviateApiKey"
|
|
||||||
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
|
||||||
placeholder="sk-123Abcweaviate"
|
|
||||||
defaultValue={settings?.WeaviateApiKey}
|
|
||||||
autoComplete="off"
|
|
||||||
spellCheck={false}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="flex w-full justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="submit"
|
|
||||||
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default memo(VectorDatabaseConnection);
|
|
@ -1,136 +0,0 @@
|
|||||||
import React, { useState } from "react";
|
|
||||||
import { X } from "@phosphor-icons/react";
|
|
||||||
import LLMSelection from "./Steps/LLMSelection";
|
|
||||||
import VectorDatabaseConnection from "./Steps/VectorDatabaseConnection";
|
|
||||||
import AppearanceSetup from "./Steps/AppearanceSetup";
|
|
||||||
import UserModeSelection from "./Steps/UserModeSelection";
|
|
||||||
import PasswordProtection from "./Steps/PasswordProtection";
|
|
||||||
import MultiUserSetup from "./Steps/MultiUserSetup";
|
|
||||||
import CreateFirstWorkspace from "./Steps/CreateFirstWorkspace";
|
|
||||||
import EmbeddingSelection from "./Steps/EmbeddingSelection";
|
|
||||||
import DataHandling from "./Steps/DataHandling";
|
|
||||||
import UserQuestionnaire from "./Steps/UserQuestionnaire";
|
|
||||||
|
|
||||||
const DIALOG_ID = "onboarding-modal";
|
|
||||||
|
|
||||||
const STEPS = {
|
|
||||||
llm_preference: {
|
|
||||||
title: "LLM Preference",
|
|
||||||
description:
|
|
||||||
"These are the credentials and settings for your preferred LLM chat & embedding provider.",
|
|
||||||
component: LLMSelection,
|
|
||||||
},
|
|
||||||
embedding_preferences: {
|
|
||||||
title: "Embedding Preference",
|
|
||||||
description: "Choose a provider for embedding files and text.",
|
|
||||||
component: EmbeddingSelection,
|
|
||||||
},
|
|
||||||
vector_database: {
|
|
||||||
title: "Vector Database",
|
|
||||||
description:
|
|
||||||
"These are the credentials and settings for how your AnythingLLM instance will function.",
|
|
||||||
component: VectorDatabaseConnection,
|
|
||||||
},
|
|
||||||
appearance: {
|
|
||||||
title: "Appearance",
|
|
||||||
description:
|
|
||||||
"Customize the appearance of your AnythingLLM instance.\nFind more customization options on the appearance settings page.",
|
|
||||||
component: AppearanceSetup,
|
|
||||||
},
|
|
||||||
user_mode_setup: {
|
|
||||||
title: "User Mode Setup",
|
|
||||||
description: "Choose how many people will be using your instance.",
|
|
||||||
component: UserModeSelection,
|
|
||||||
},
|
|
||||||
password_protection: {
|
|
||||||
title: "Password Protect",
|
|
||||||
description:
|
|
||||||
"Protect your instance with a password. It is important to save this password as it cannot be recovered.",
|
|
||||||
component: PasswordProtection,
|
|
||||||
},
|
|
||||||
multi_user_mode: {
|
|
||||||
title: "Multi-User Mode",
|
|
||||||
description:
|
|
||||||
"Setup your instance to support your team by activating multi-user mode.",
|
|
||||||
component: MultiUserSetup,
|
|
||||||
},
|
|
||||||
data_handling: {
|
|
||||||
title: "Data Handling",
|
|
||||||
description:
|
|
||||||
"We are committed to transparency and control when it comes to your personal data.",
|
|
||||||
component: DataHandling,
|
|
||||||
},
|
|
||||||
user_questionnaire: {
|
|
||||||
title: "A little about yourself",
|
|
||||||
description:
|
|
||||||
"We use information about how you use AnythingLLM to make our product better.",
|
|
||||||
component: UserQuestionnaire,
|
|
||||||
},
|
|
||||||
create_workspace: {
|
|
||||||
title: "Create Workspace",
|
|
||||||
description: "To get started, create a new workspace.",
|
|
||||||
component: CreateFirstWorkspace,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export const OnboardingModalId = DIALOG_ID;
|
|
||||||
export default function OnboardingModal({ setModalVisible }) {
|
|
||||||
const [currentStep, setCurrentStep] = useState("llm_preference");
|
|
||||||
const [history, setHistory] = useState(["llm_preference"]);
|
|
||||||
|
|
||||||
function hideModal() {
|
|
||||||
setModalVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const nextStep = (stepKey) => {
|
|
||||||
setCurrentStep(stepKey);
|
|
||||||
setHistory([...history, stepKey]);
|
|
||||||
};
|
|
||||||
|
|
||||||
const prevStep = () => {
|
|
||||||
const currentStepIdx = history.indexOf(currentStep);
|
|
||||||
if (currentStepIdx === -1 || currentStepIdx === 0) {
|
|
||||||
setCurrentStep("llm_preference");
|
|
||||||
setHistory(["llm_preference"]);
|
|
||||||
return hideModal();
|
|
||||||
}
|
|
||||||
|
|
||||||
const prevStep = history[currentStepIdx - 1];
|
|
||||||
const _history = [...history].slice(0, currentStepIdx);
|
|
||||||
setCurrentStep(prevStep);
|
|
||||||
setHistory(_history);
|
|
||||||
};
|
|
||||||
|
|
||||||
const { component: StepComponent, ...step } = STEPS[currentStep];
|
|
||||||
return (
|
|
||||||
<dialog id={DIALOG_ID} className="bg-transparent outline-none">
|
|
||||||
<div className="relative max-h-full">
|
|
||||||
<div className="relative bg-main-gradient rounded-2xl shadow border-2 border-slate-300/10">
|
|
||||||
<div className="flex items-start justify-between px-6 py-4 border-b rounded-t border-gray-500/50">
|
|
||||||
<div className="flex flex-col gap-2">
|
|
||||||
<h3 className="text-xl font-semibold text-white">{step.title}</h3>
|
|
||||||
<p className="text-sm font-base text-white text-opacity-60 whitespace-pre">
|
|
||||||
{step.description || ""}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={hideModal}
|
|
||||||
type="button"
|
|
||||||
className="text-gray-400 bg-transparent rounded-lg text-sm p-1.5 ml-auto inline-flex items-center hover:border-white/60 bg-sidebar-button hover:bg-menu-item-selected-gradient hover:border-slate-100 hover:border-opacity-50 border-transparent border"
|
|
||||||
>
|
|
||||||
<X className="text-gray-300 text-lg" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-6 flex h-full w-full justify-center">
|
|
||||||
<StepComponent
|
|
||||||
currentStep={currentStep}
|
|
||||||
nextStep={nextStep}
|
|
||||||
prevStep={prevStep}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</dialog>
|
|
||||||
);
|
|
||||||
}
|
|
@ -0,0 +1,99 @@
|
|||||||
|
import React, { useEffect, useRef, useState } from "react";
|
||||||
|
import illustration from "@/media/illustrations/create-workspace.png";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import Workspace from "@/models/workspace";
|
||||||
|
|
||||||
|
const TITLE = "Create your first workspace";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"Create your first workspace and get started with AnythingLLM.";
|
||||||
|
|
||||||
|
export default function CreateWorkspace({
|
||||||
|
setHeader,
|
||||||
|
setForwardBtn,
|
||||||
|
setBackBtn,
|
||||||
|
}) {
|
||||||
|
const [workspaceName, setWorkspaceName] = useState("");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const createWorkspaceRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setBackBtn({ showing: false, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (workspaceName.length > 3) {
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
} else {
|
||||||
|
setForwardBtn({ showing: true, disabled: true, onClick: handleForward });
|
||||||
|
}
|
||||||
|
}, [workspaceName]);
|
||||||
|
|
||||||
|
const handleCreate = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
const { workspace, error } = await Workspace.new({
|
||||||
|
name: form.get("name"),
|
||||||
|
onboardingComplete: true,
|
||||||
|
});
|
||||||
|
if (!!workspace) {
|
||||||
|
showToast(
|
||||||
|
"Workspace created successfully! Taking you to home...",
|
||||||
|
"success"
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||||
|
navigate(paths.home());
|
||||||
|
} else {
|
||||||
|
showToast(`Failed to create workspace: ${error}`, "error");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
createWorkspaceRef.current.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.survey());
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={handleCreate}
|
||||||
|
className="w-full flex items-center justify-center flex-col gap-y-2"
|
||||||
|
>
|
||||||
|
<img src={illustration} alt="Create workspace" />
|
||||||
|
<div className="flex flex-col gap-y-4 w-full max-w-[600px]">
|
||||||
|
{" "}
|
||||||
|
<div className="w-full mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-3 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Workspace Name
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="name"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
placeholder="My Workspace"
|
||||||
|
minLength={4}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => setWorkspaceName(e.target.value)}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 text-white text-opacity-80 text-xs font-base -mb-2">
|
||||||
|
Workspace name must be at least 4 characters.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={createWorkspaceRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
}
|
136
frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx
Normal file
136
frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx
Normal file
@ -0,0 +1,136 @@
|
|||||||
|
import useLogo from "@/hooks/useLogo";
|
||||||
|
import System from "@/models/system";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { Plus } from "@phosphor-icons/react";
|
||||||
|
import React, { useState, useEffect } from "react";
|
||||||
|
import AnythingLLM from "@/media/logo/anything-llm.png";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const TITLE = "Custom Logo";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"Upload your custom logo to make your chatbot yours. Optional.";
|
||||||
|
|
||||||
|
export default function CustomLogo({ setHeader, setForwardBtn, setBackBtn }) {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
function handleForward() {
|
||||||
|
navigate(paths.onboarding.userSetup());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.vectorDatabase());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const { logo: _initLogo, setLogo: _setLogo } = useLogo();
|
||||||
|
const [logo, setLogo] = useState("");
|
||||||
|
const [isDefaultLogo, setIsDefaultLogo] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function logoInit() {
|
||||||
|
setLogo(_initLogo || "");
|
||||||
|
const _isDefaultLogo = await System.isDefaultLogo();
|
||||||
|
setIsDefaultLogo(_isDefaultLogo);
|
||||||
|
}
|
||||||
|
logoInit();
|
||||||
|
}, [_initLogo]);
|
||||||
|
|
||||||
|
const handleFileUpload = async (event) => {
|
||||||
|
const file = event.target.files[0];
|
||||||
|
if (!file) return false;
|
||||||
|
|
||||||
|
const objectURL = URL.createObjectURL(file);
|
||||||
|
setLogo(objectURL);
|
||||||
|
|
||||||
|
const formData = new FormData();
|
||||||
|
formData.append("logo", file);
|
||||||
|
const { success, error } = await System.uploadLogo(formData);
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Failed to upload logo: ${error}`, "error");
|
||||||
|
setLogo(_initLogo);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
_setLogo(logoURL);
|
||||||
|
|
||||||
|
showToast("Image uploaded successfully.", "success", { clear: true });
|
||||||
|
setIsDefaultLogo(false);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRemoveLogo = async () => {
|
||||||
|
setLogo("");
|
||||||
|
setIsDefaultLogo(true);
|
||||||
|
|
||||||
|
const { success, error } = await System.removeCustomLogo();
|
||||||
|
if (!success) {
|
||||||
|
console.error("Failed to remove logo:", error);
|
||||||
|
showToast(`Failed to remove logo: ${error}`, "error");
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
setLogo(logoURL);
|
||||||
|
setIsDefaultLogo(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const logoURL = await System.fetchLogo();
|
||||||
|
_setLogo(logoURL);
|
||||||
|
|
||||||
|
showToast("Image successfully removed.", "success", { clear: true });
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex items-center w-full">
|
||||||
|
<div className="flex gap-x-8 flex-col w-full">
|
||||||
|
{isDefaultLogo ? (
|
||||||
|
<label className="mt-5 hover:opacity-60 w-full flex justify-center transition-all duration-300">
|
||||||
|
<input
|
||||||
|
id="logo-upload"
|
||||||
|
type="file"
|
||||||
|
accept="image/*"
|
||||||
|
className="hidden"
|
||||||
|
onChange={handleFileUpload}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className="max-w-[600px] w-full h-64 max-h-[600px] py-4 bg-zinc-900/50 rounded-2xl border-2 border-dashed border-white border-opacity-60 justify-center items-center inline-flex cursor-pointer"
|
||||||
|
htmlFor="logo-upload"
|
||||||
|
>
|
||||||
|
<div className="flex flex-col items-center justify-center">
|
||||||
|
<div className="rounded-full bg-white/40">
|
||||||
|
<Plus className="w-6 h-6 text-black/80 m-2" />
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-80 text-sm font-semibold py-1">
|
||||||
|
Add a custom logo
|
||||||
|
</div>
|
||||||
|
<div className="text-white text-opacity-60 text-xs font-medium py-1">
|
||||||
|
Recommended size: 800 x 200
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
) : (
|
||||||
|
<div className="w-full flex justify-center">
|
||||||
|
<img
|
||||||
|
src={logo}
|
||||||
|
alt="Uploaded Logo"
|
||||||
|
className="w-48 h-48 object-contain mr-6"
|
||||||
|
hidden={isDefaultLogo}
|
||||||
|
onError={(e) => (e.target.src = AnythingLLM)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<button
|
||||||
|
onClick={handleRemoveLogo}
|
||||||
|
className="text-white text-base font-medium hover:text-opacity-60 mt-8"
|
||||||
|
>
|
||||||
|
Remove logo
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,4 +1,4 @@
|
|||||||
import React, { memo, useEffect, useState } from "react";
|
import PreLoader from "@/components/Preloader";
|
||||||
import System from "@/models/system";
|
import System from "@/models/system";
|
||||||
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||||
import OpenAiLogo from "@/media/llmprovider/openai.png";
|
import OpenAiLogo from "@/media/llmprovider/openai.png";
|
||||||
@ -13,8 +13,13 @@ import PineconeLogo from "@/media/vectordbs/pinecone.png";
|
|||||||
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
|
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
|
||||||
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
|
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
|
||||||
import QDrantLogo from "@/media/vectordbs/qdrant.png";
|
import QDrantLogo from "@/media/vectordbs/qdrant.png";
|
||||||
import PreLoader from "@/components/Preloader";
|
import React, { useState, useEffect } from "react";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const TITLE = "Data Handling & Privacy";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"We are committed to transparency and control when it comes to your personal data.";
|
||||||
const LLM_SELECTION_PRIVACY = {
|
const LLM_SELECTION_PRIVACY = {
|
||||||
openai: {
|
openai: {
|
||||||
name: "OpenAI",
|
name: "OpenAI",
|
||||||
@ -151,26 +156,36 @@ const EMBEDDING_ENGINE_PRIVACY = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function DataHandling({ nextStep, prevStep, currentStep }) {
|
export default function DataHandling({ setHeader, setForwardBtn, setBackBtn }) {
|
||||||
const [llmChoice, setLLMChoice] = useState("openai");
|
const [llmChoice, setLLMChoice] = useState("openai");
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
const [vectorDb, setVectorDb] = useState("pinecone");
|
const [vectorDb, setVectorDb] = useState("pinecone");
|
||||||
const [embeddingEngine, setEmbeddingEngine] = useState("openai");
|
const [embeddingEngine, setEmbeddingEngine] = useState("openai");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
setBackBtn({ showing: false, disabled: false, onClick: handleBack });
|
||||||
async function fetchKeys() {
|
async function fetchKeys() {
|
||||||
const _settings = await System.keys();
|
const _settings = await System.keys();
|
||||||
setLLMChoice(_settings?.LLMProvider);
|
setLLMChoice(_settings?.LLMProvider || "openai");
|
||||||
setVectorDb(_settings?.VectorDB);
|
setVectorDb(_settings?.VectorDB || "pinecone");
|
||||||
setEmbeddingEngine(_settings?.EmbeddingEngine);
|
setEmbeddingEngine(_settings?.EmbeddingEngine || "openai");
|
||||||
|
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
if (currentStep === "data_handling") {
|
|
||||||
fetchKeys();
|
fetchKeys();
|
||||||
}
|
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
navigate(paths.onboarding.survey());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.userSetup());
|
||||||
|
}
|
||||||
|
|
||||||
if (loading)
|
if (loading)
|
||||||
return (
|
return (
|
||||||
<div className="w-full h-full flex justify-center items-center p-20">
|
<div className="w-full h-full flex justify-center items-center p-20">
|
||||||
@ -179,7 +194,7 @@ function DataHandling({ nextStep, prevStep, currentStep }) {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-[750px]">
|
<div className="w-full flex items-center justify-center flex-col gap-y-6">
|
||||||
<div className="p-8 flex flex-col gap-8">
|
<div className="p-8 flex flex-col gap-8">
|
||||||
<div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4">
|
<div className="flex flex-col gap-y-2 border-b border-zinc-500/50 pb-4">
|
||||||
<div className="text-white text-base font-bold">LLM Selection</div>
|
<div className="text-white text-base font-bold">LLM Selection</div>
|
||||||
@ -239,23 +254,6 @@ function DataHandling({ nextStep, prevStep, currentStep }) {
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-[650px] justify-between items-center px-6 py-4 space-x-2 border-t rounded-b border-gray-500/50">
|
|
||||||
<button
|
|
||||||
onClick={prevStep}
|
|
||||||
type="button"
|
|
||||||
className="px-4 py-2 rounded-lg text-white hover:bg-sidebar"
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => nextStep("user_questionnaire")}
|
|
||||||
className="border border-slate-200 px-4 py-2 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow"
|
|
||||||
>
|
|
||||||
Continue
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(DataHandling);
|
|
@ -0,0 +1,39 @@
|
|||||||
|
export default function EmbedderItem({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => onClick(value)}
|
||||||
|
className={`w-full hover:bg-white/10 p-2 rounded-md hover:cursor-pointer ${
|
||||||
|
checked && "bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={value}
|
||||||
|
className="peer hidden"
|
||||||
|
checked={checked}
|
||||||
|
readOnly={true}
|
||||||
|
formNoValidate={true}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-x-4 items-center">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`${name} logo`}
|
||||||
|
className="w-10 h-10 rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<div className="text-sm font-semibold">{name}</div>
|
||||||
|
<div className="mt-2 text-xs text-white tracking-wide">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,180 @@
|
|||||||
|
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||||
|
import OpenAiLogo from "@/media/llmprovider/openai.png";
|
||||||
|
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
|
||||||
|
import LocalAiLogo from "@/media/llmprovider/localai.png";
|
||||||
|
import NativeEmbeddingOptions from "@/components/EmbeddingSelection/NativeEmbeddingOptions";
|
||||||
|
import OpenAiOptions from "@/components/EmbeddingSelection/OpenAiOptions";
|
||||||
|
import AzureAiOptions from "@/components/EmbeddingSelection/AzureAiOptions";
|
||||||
|
import LocalAiOptions from "@/components/EmbeddingSelection/LocalAiOptions";
|
||||||
|
import EmbedderItem from "./EmbedderItem";
|
||||||
|
import System from "@/models/system";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const TITLE = "Embedding Preference";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"AnythingLLM can work with many embedding models. This will be the model which turns documents into vectors.";
|
||||||
|
|
||||||
|
export default function EmbeddingPreference({
|
||||||
|
setHeader,
|
||||||
|
setForwardBtn,
|
||||||
|
setBackBtn,
|
||||||
|
}) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [filteredEmbedders, setFilteredEmbedders] = useState([]);
|
||||||
|
const [selectedEmbedder, setSelectedEmbedder] = useState(null);
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const hiddenSubmitButtonRef = useRef(null);
|
||||||
|
const isHosted = window.location.hostname.includes("useanything.com");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setSelectedEmbedder(_settings?.EmbeddingEngine || "native");
|
||||||
|
}
|
||||||
|
fetchKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const EMBEDDERS = [
|
||||||
|
{
|
||||||
|
name: "AnythingLLM Embedder",
|
||||||
|
value: "native",
|
||||||
|
logo: AnythingLLMIcon,
|
||||||
|
options: <NativeEmbeddingOptions settings={settings} />,
|
||||||
|
description:
|
||||||
|
"Use the built-in embedding engine for AnythingLLM. Zero setup!",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "OpenAI",
|
||||||
|
value: "openai",
|
||||||
|
logo: OpenAiLogo,
|
||||||
|
options: <OpenAiOptions settings={settings} />,
|
||||||
|
description: "The standard option for most non-commercial use.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Azure OpenAI",
|
||||||
|
value: "azure",
|
||||||
|
logo: AzureOpenAiLogo,
|
||||||
|
options: <AzureAiOptions settings={settings} />,
|
||||||
|
description: "The enterprise option of OpenAI hosted on Azure services.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local AI",
|
||||||
|
value: "localai",
|
||||||
|
logo: LocalAiLogo,
|
||||||
|
options: <LocalAiOptions settings={settings} />,
|
||||||
|
description: "Run embedding models locally on your own machine.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
if (hiddenSubmitButtonRef.current) {
|
||||||
|
hiddenSubmitButtonRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.llmPreference());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const data = {};
|
||||||
|
const formData = new FormData(form);
|
||||||
|
data.EmbeddingEngine = selectedEmbedder;
|
||||||
|
for (var [key, value] of formData.entries()) data[key] = value;
|
||||||
|
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
showToast(`Failed to save embedding settings: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast("Embedder settings saved successfully.", "success", {
|
||||||
|
clear: true,
|
||||||
|
});
|
||||||
|
navigate(paths.onboarding.vectorDatabase());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchQuery.trim() === "") {
|
||||||
|
setFilteredEmbedders(EMBEDDERS);
|
||||||
|
} else {
|
||||||
|
const lowercasedQuery = searchQuery.toLowerCase();
|
||||||
|
const filtered = EMBEDDERS.filter((embedder) =>
|
||||||
|
embedder.name.toLowerCase().includes(lowercasedQuery)
|
||||||
|
);
|
||||||
|
setFilteredEmbedders(filtered);
|
||||||
|
}
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form ref={formRef} onSubmit={handleSubmit} className="w-full">
|
||||||
|
<div className="w-full relative border-slate-300/40 shadow border-2 rounded-lg text-white">
|
||||||
|
<div className="w-full p-4 absolute top-0 rounded-t-lg bg-accent/50">
|
||||||
|
<div className="w-full flex items-center sticky top-0 z-20">
|
||||||
|
<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 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) => {
|
||||||
|
if (embedder.value === "native" && isHosted) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<EmbedderItem
|
||||||
|
key={embedder.name}
|
||||||
|
name={embedder.name}
|
||||||
|
value={embedder.value}
|
||||||
|
image={embedder.logo}
|
||||||
|
description={embedder.description}
|
||||||
|
checked={selectedEmbedder === embedder.value}
|
||||||
|
onClick={() => setSelectedEmbedder(embedder.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-col gap-y-1">
|
||||||
|
{selectedEmbedder &&
|
||||||
|
EMBEDDERS.find((embedder) => embedder.value === selectedEmbedder)
|
||||||
|
?.options}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={hiddenSubmitButtonRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
41
frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx
Normal file
41
frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
import paths from "@/utils/paths";
|
||||||
|
import LGroupImg from "./l_group.png";
|
||||||
|
import RGroupImg from "./r_group.png";
|
||||||
|
import AnythingLLMLogo from "@/media/logo/anything-llm.png";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
export default function OnboardingHome() {
|
||||||
|
const navigate = useNavigate();
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="relative w-screen h-screen flex overflow-hidden bg-[#2C2F35] md:bg-main-gradient">
|
||||||
|
<div
|
||||||
|
className="hidden md:block fixed bottom-10 left-10 w-[320px] h-[320px] bg-no-repeat bg-contain"
|
||||||
|
style={{ backgroundImage: `url(${LGroupImg})` }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className="hidden md:block fixed top-10 right-10 w-[320px] h-[320px] bg-no-repeat bg-contain"
|
||||||
|
style={{ backgroundImage: `url(${RGroupImg})` }}
|
||||||
|
></div>
|
||||||
|
|
||||||
|
<div className="relative flex justify-center items-center m-auto">
|
||||||
|
<div className="flex flex-col justify-center items-center">
|
||||||
|
<p className="text-zinc-300 font-thin text-[24px]">Welcome to</p>
|
||||||
|
<img
|
||||||
|
src={AnythingLLMLogo}
|
||||||
|
alt="AnythingLLM"
|
||||||
|
className="md:h-[50px] flex-shrink-0 max-w-[300px]"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={() => navigate(paths.onboarding.llmPreference())}
|
||||||
|
className="animate-pulse w-full md:max-w-[350px] md:min-w-[300px] text-center py-3 bg-white text-black font-semibold text-sm my-10 rounded-md hover:bg-gray-200"
|
||||||
|
>
|
||||||
|
Get started
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
BIN
frontend/src/pages/OnboardingFlow/Steps/Home/l_group.png
Normal file
BIN
frontend/src/pages/OnboardingFlow/Steps/Home/l_group.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 72 KiB |
BIN
frontend/src/pages/OnboardingFlow/Steps/Home/r_group.png
Normal file
BIN
frontend/src/pages/OnboardingFlow/Steps/Home/r_group.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 80 KiB |
@ -0,0 +1,39 @@
|
|||||||
|
export default function LLMItem({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => onClick(value)}
|
||||||
|
className={`w-full hover:bg-white/10 p-2 rounded-md hover:cursor-pointer ${
|
||||||
|
checked && "bg-white/10"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={value}
|
||||||
|
className="peer hidden"
|
||||||
|
checked={checked}
|
||||||
|
readOnly={true}
|
||||||
|
formNoValidate={true}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-x-4 items-center">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`${name} logo`}
|
||||||
|
className="w-10 h-10 rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<div className="text-sm font-semibold">{name}</div>
|
||||||
|
<div className="mt-2 text-xs text-white tracking-wide">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
211
frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx
Normal file
211
frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx
Normal file
@ -0,0 +1,211 @@
|
|||||||
|
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||||
|
import { useEffect, useState, useRef } from "react";
|
||||||
|
import OpenAiLogo from "@/media/llmprovider/openai.png";
|
||||||
|
import AzureOpenAiLogo from "@/media/llmprovider/azure.png";
|
||||||
|
import AnthropicLogo from "@/media/llmprovider/anthropic.png";
|
||||||
|
import GeminiLogo from "@/media/llmprovider/gemini.png";
|
||||||
|
import OllamaLogo from "@/media/llmprovider/ollama.png";
|
||||||
|
import LMStudioLogo from "@/media/llmprovider/lmstudio.png";
|
||||||
|
import LocalAiLogo from "@/media/llmprovider/localai.png";
|
||||||
|
import AnythingLLMIcon from "@/media/logo/anything-llm-icon.png";
|
||||||
|
import OpenAiOptions from "@/components/LLMSelection/OpenAiOptions";
|
||||||
|
import AzureAiOptions from "@/components/LLMSelection/AzureAiOptions";
|
||||||
|
import AnthropicAiOptions from "@/components/LLMSelection/AnthropicAiOptions";
|
||||||
|
import LMStudioOptions from "@/components/LLMSelection/LMStudioOptions";
|
||||||
|
import LocalAiOptions from "@/components/LLMSelection/LocalAiOptions";
|
||||||
|
import NativeLLMOptions from "@/components/LLMSelection/NativeLLMOptions";
|
||||||
|
import GeminiLLMOptions from "@/components/LLMSelection/GeminiLLMOptions";
|
||||||
|
import OllamaLLMOptions from "@/components/LLMSelection/OllamaLLMOptions";
|
||||||
|
import LLMItem from "./LLMItem";
|
||||||
|
import System from "@/models/system";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const TITLE = "LLM Preference";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"AnythingLLM can work with many LLM providers. This will be the service which handles chatting.";
|
||||||
|
|
||||||
|
export default function LLMPreference({
|
||||||
|
setHeader,
|
||||||
|
setForwardBtn,
|
||||||
|
setBackBtn,
|
||||||
|
}) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [filteredLLMs, setFilteredLLMs] = useState([]);
|
||||||
|
const [selectedLLM, setSelectedLLM] = useState(null);
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const hiddenSubmitButtonRef = useRef(null);
|
||||||
|
const isHosted = window.location.hostname.includes("useanything.com");
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setSelectedLLM(_settings?.LLMProvider || "openai");
|
||||||
|
}
|
||||||
|
fetchKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const LLMS = [
|
||||||
|
{
|
||||||
|
name: "OpenAI",
|
||||||
|
value: "openai",
|
||||||
|
logo: OpenAiLogo,
|
||||||
|
options: <OpenAiOptions settings={settings} />,
|
||||||
|
description: "The standard option for most non-commercial use.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Azure OpenAI",
|
||||||
|
value: "azure",
|
||||||
|
logo: AzureOpenAiLogo,
|
||||||
|
options: <AzureAiOptions settings={settings} />,
|
||||||
|
description: "The enterprise option of OpenAI hosted on Azure services.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Anthropic",
|
||||||
|
value: "anthropic",
|
||||||
|
logo: AnthropicLogo,
|
||||||
|
options: <AnthropicAiOptions settings={settings} />,
|
||||||
|
description: "A friendly AI Assistant hosted by Anthropic.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Gemini",
|
||||||
|
value: "gemini",
|
||||||
|
logo: GeminiLogo,
|
||||||
|
options: <GeminiLLMOptions settings={settings} />,
|
||||||
|
description: "Google's largest and most capable AI model",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Ollama",
|
||||||
|
value: "ollama",
|
||||||
|
logo: OllamaLogo,
|
||||||
|
options: <OllamaLLMOptions settings={settings} />,
|
||||||
|
description: "Run LLMs locally on your own machine.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "LM Studio",
|
||||||
|
value: "lmstudio",
|
||||||
|
logo: LMStudioLogo,
|
||||||
|
options: <LMStudioOptions settings={settings} />,
|
||||||
|
description:
|
||||||
|
"Discover, download, and run thousands of cutting edge LLMs in a few clicks.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Local AI",
|
||||||
|
value: "localai",
|
||||||
|
logo: LocalAiLogo,
|
||||||
|
options: <LocalAiOptions settings={settings} />,
|
||||||
|
description: "Run LLMs locally on your own machine.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Native",
|
||||||
|
value: "native",
|
||||||
|
logo: AnythingLLMIcon,
|
||||||
|
options: <NativeLLMOptions settings={settings} />,
|
||||||
|
description:
|
||||||
|
"Use a downloaded custom Llama model for chatting on this AnythingLLM instance.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
if (hiddenSubmitButtonRef.current) {
|
||||||
|
hiddenSubmitButtonRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.home());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const data = {};
|
||||||
|
const formData = new FormData(form);
|
||||||
|
data.LLMProvider = selectedLLM;
|
||||||
|
for (var [key, value] of formData.entries()) data[key] = value;
|
||||||
|
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
showToast(`Failed to save LLM settings: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast("LLM settings saved successfully.", "success", { clear: true });
|
||||||
|
navigate(paths.onboarding.embeddingPreference());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchQuery.trim() === "") {
|
||||||
|
setFilteredLLMs(LLMS);
|
||||||
|
} else {
|
||||||
|
const lowercasedQuery = searchQuery.toLowerCase();
|
||||||
|
const filtered = LLMS.filter((llm) =>
|
||||||
|
llm.name.toLowerCase().includes(lowercasedQuery)
|
||||||
|
);
|
||||||
|
setFilteredLLMs(filtered);
|
||||||
|
}
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<form ref={formRef} onSubmit={handleSubmit} className="w-full">
|
||||||
|
<div className="w-full relative border-slate-300/40 shadow border-2 rounded-lg text-white">
|
||||||
|
<div className="w-full p-4 absolute top-0 rounded-t-lg bg-accent/50">
|
||||||
|
<div className="w-full flex items-center sticky top-0 z-20">
|
||||||
|
<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 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={() => setSelectedLLM(llm.value)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-col gap-y-1">
|
||||||
|
{selectedLLM &&
|
||||||
|
LLMS.find((llm) => llm.value === selectedLLM)?.options}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={hiddenSubmitButtonRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
297
frontend/src/pages/OnboardingFlow/Steps/Survey/index.jsx
Normal file
297
frontend/src/pages/OnboardingFlow/Steps/Survey/index.jsx
Normal file
@ -0,0 +1,297 @@
|
|||||||
|
import { COMPLETE_QUESTIONNAIRE } from "@/utils/constants";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import { CheckCircle } from "@phosphor-icons/react";
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const TITLE = "Welcome to AnythingLLM";
|
||||||
|
const DESCRIPTION = "Help us make AnythingLLM built for your needs. Optional.";
|
||||||
|
|
||||||
|
async function sendQuestionnaire({ email, useCase, comment }) {
|
||||||
|
if (import.meta.env.DEV) return;
|
||||||
|
return fetch(`https://onboarding-wxich7363q-uc.a.run.app`, {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
email,
|
||||||
|
useCase,
|
||||||
|
comment,
|
||||||
|
sourceId: "0VRjqHh6Vukqi0x0Vd0n/m8JuT7k8nOz",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
window.localStorage.setItem(COMPLETE_QUESTIONNAIRE, true);
|
||||||
|
console.log(`✅ Questionnaire responses sent.`);
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error(`sendQuestionnaire`, error.message);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function Survey({ setHeader, setForwardBtn, setBackBtn }) {
|
||||||
|
const [selectedOption, setSelectedOption] = useState("");
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
const submitRef = useRef(null);
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {
|
||||||
|
navigate(paths.onboarding.createWorkspace());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (submitRef.current) {
|
||||||
|
submitRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function skipSurvey() {
|
||||||
|
navigate(paths.onboarding.createWorkspace());
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.dataHandling());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
|
||||||
|
await sendQuestionnaire({
|
||||||
|
email: formData.get("email"),
|
||||||
|
useCase: formData.get("use_case") || "other",
|
||||||
|
comment: formData.get("comment") || null,
|
||||||
|
});
|
||||||
|
|
||||||
|
navigate(paths.onboarding.createWorkspace());
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!!window?.localStorage?.getItem(COMPLETE_QUESTIONNAIRE)) {
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-center items-center py-40">
|
||||||
|
<div className="w-full flex items-center justify-center px-1 md:px-8 py-4">
|
||||||
|
<div className="w-auto flex flex-col gap-y-1 items-center">
|
||||||
|
<CheckCircle size={60} className="text-green-500" />
|
||||||
|
<p className="text-white text-lg">Thank you for your feedback!</p>
|
||||||
|
<a
|
||||||
|
href={paths.mailToMintplex()}
|
||||||
|
className="text-sky-400 underline text-xs"
|
||||||
|
>
|
||||||
|
team@mintplexlabs.com
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex justify-center">
|
||||||
|
<form onSubmit={handleSubmit} ref={formRef} className="">
|
||||||
|
<div className="md:min-w-[400px]">
|
||||||
|
<label htmlFor="email" className="text-white text-base font-medium">
|
||||||
|
What's your email?{" "}
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="email"
|
||||||
|
type="email"
|
||||||
|
placeholder="you@gmail.com"
|
||||||
|
required={true}
|
||||||
|
className="mt-2 bg-zinc-900 text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight w-full h-11 p-2.5 bg-zinc-900 rounded-lg"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<label
|
||||||
|
className="text-white text-base font-medium"
|
||||||
|
htmlFor="use_case"
|
||||||
|
>
|
||||||
|
What will you use AnythingLLM for?{" "}
|
||||||
|
</label>
|
||||||
|
<div className="mt-2 gap-y-3 flex flex-col">
|
||||||
|
<label
|
||||||
|
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||||
|
selectedOption === "business"
|
||||||
|
? "border-white border-opacity-40"
|
||||||
|
: ""
|
||||||
|
} hover:border-white/60`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="use_case"
|
||||||
|
value={"business"}
|
||||||
|
checked={selectedOption === "business"}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||||
|
selectedOption === "business" ? "bg-white" : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||||
|
For my business
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||||
|
selectedOption === "personal"
|
||||||
|
? "border-white border-opacity-40"
|
||||||
|
: ""
|
||||||
|
} hover:border-white/60`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="use_case"
|
||||||
|
value={"personal"}
|
||||||
|
checked={selectedOption === "personal"}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||||
|
selectedOption === "personal" ? "bg-white" : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||||
|
For personal use
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||||
|
selectedOption === "education"
|
||||||
|
? "border-white border-opacity-40"
|
||||||
|
: ""
|
||||||
|
} hover:border-white/60`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="use_case"
|
||||||
|
value={"education"}
|
||||||
|
checked={selectedOption === "education"}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||||
|
selectedOption === "education" ? "bg-white" : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||||
|
For my education
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||||
|
selectedOption === "side_hustle"
|
||||||
|
? "border-white border-opacity-40"
|
||||||
|
: ""
|
||||||
|
} hover:border-white/60`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="use_case"
|
||||||
|
value={"side_hustle"}
|
||||||
|
checked={selectedOption === "side_hustle"}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||||
|
selectedOption === "side_hustle" ? "bg-white" : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||||
|
For my side-hustle
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||||
|
selectedOption === "job" ? "border-white border-opacity-40" : ""
|
||||||
|
} hover:border-white/60`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="use_case"
|
||||||
|
value={"job"}
|
||||||
|
checked={selectedOption === "job"}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||||
|
selectedOption === "job" ? "bg-white" : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||||
|
For my job
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
<label
|
||||||
|
className={`transition-all duration-300 w-full h-11 p-2.5 bg-white/10 rounded-lg flex justify-start items-center gap-2.5 cursor-pointer border border-transparent ${
|
||||||
|
selectedOption === "other"
|
||||||
|
? "border-white border-opacity-40"
|
||||||
|
: ""
|
||||||
|
} hover:border-white/60`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="radio"
|
||||||
|
name="use_case"
|
||||||
|
value={"other"}
|
||||||
|
checked={selectedOption === "other"}
|
||||||
|
onChange={(e) => setSelectedOption(e.target.value)}
|
||||||
|
className="hidden"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={`w-4 h-4 rounded-full border-2 border-white mr-2 ${
|
||||||
|
selectedOption === "other" ? "bg-white" : ""
|
||||||
|
}`}
|
||||||
|
></div>
|
||||||
|
<div className="text-white text-sm font-medium font-['Plus Jakarta Sans'] leading-tight">
|
||||||
|
Other
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="mt-8">
|
||||||
|
<label htmlFor="comment" className="text-white text-base font-medium">
|
||||||
|
Any comments for the team?{" "}
|
||||||
|
<span className="text-neutral-400 text-base font-light">
|
||||||
|
(Optional)
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
name="comment"
|
||||||
|
rows={5}
|
||||||
|
className="mt-2 bg-zinc-900 text-white text-sm rounded-lg focus:ring-blue-500 focus:border-blue-500 block w-full p-2.5"
|
||||||
|
placeholder="If you have any questions or comments right now, you can leave them here and we will get back to you. You can also email team@mintplexlabs.com"
|
||||||
|
wrap="soft"
|
||||||
|
autoComplete="off"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={submitRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
|
||||||
|
<div className="w-full flex items-center justify-center">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={skipSurvey}
|
||||||
|
className="text-white text-base font-medium text-opacity-30 hover:text-opacity-100 mt-8"
|
||||||
|
>
|
||||||
|
Skip Survey
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
336
frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx
Normal file
336
frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx
Normal file
@ -0,0 +1,336 @@
|
|||||||
|
import System from "@/models/system";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import React, { useState, useEffect, useRef } from "react";
|
||||||
|
import debounce from "lodash.debounce";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
import { AUTH_TIMESTAMP, AUTH_TOKEN, AUTH_USER } from "@/utils/constants";
|
||||||
|
|
||||||
|
const TITLE = "User Setup";
|
||||||
|
const DESCRIPTION = "Configure your user settings.";
|
||||||
|
|
||||||
|
export default function UserSetup({ setHeader, setForwardBtn, setBackBtn }) {
|
||||||
|
const [selectedOption, setSelectedOption] = useState("");
|
||||||
|
const [singleUserPasswordValid, setSingleUserPasswordValid] = useState(false);
|
||||||
|
const [multiUserLoginValid, setMultiUserLoginValid] = useState(false);
|
||||||
|
const [enablePassword, setEnablePassword] = useState(false);
|
||||||
|
const myTeamSubmitRef = useRef(null);
|
||||||
|
const justMeSubmitRef = useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
if (selectedOption === "just_me" && enablePassword) {
|
||||||
|
justMeSubmitRef.current?.click();
|
||||||
|
} else if (selectedOption === "just_me" && !enablePassword) {
|
||||||
|
navigate(paths.onboarding.dataHandling());
|
||||||
|
} else if (selectedOption === "my_team") {
|
||||||
|
myTeamSubmitRef.current?.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.customLogo());
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
let isDisabled = true;
|
||||||
|
if (selectedOption === "just_me") {
|
||||||
|
isDisabled = !singleUserPasswordValid;
|
||||||
|
} else if (selectedOption === "my_team") {
|
||||||
|
isDisabled = !multiUserLoginValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
setForwardBtn({
|
||||||
|
showing: true,
|
||||||
|
disabled: isDisabled,
|
||||||
|
onClick: handleForward,
|
||||||
|
});
|
||||||
|
}, [selectedOption, singleUserPasswordValid, multiUserLoginValid]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full flex items-center justify-center flex-col gap-y-6">
|
||||||
|
<div className="flex flex-col border rounded-lg border-white/20 p-8 items-center gap-y-4 w-full max-w-[600px]">
|
||||||
|
<div className=" text-white text-sm font-semibold md:-ml-44">
|
||||||
|
How many people will be using your instance?
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 w-full justify-center">
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedOption("just_me")}
|
||||||
|
className={`${
|
||||||
|
selectedOption === "just_me"
|
||||||
|
? "text-sky-400 border-sky-400/70"
|
||||||
|
: "text-white border-white/40"
|
||||||
|
} min-w-[230px] h-11 p-4 rounded-[10px] border-2 justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}
|
||||||
|
>
|
||||||
|
<div className="text-center text-sm font-bold">Just me</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={() => setSelectedOption("my_team")}
|
||||||
|
className={`${
|
||||||
|
selectedOption === "my_team"
|
||||||
|
? "text-sky-400 border-sky-400/70"
|
||||||
|
: "text-white border-white/40"
|
||||||
|
} min-w-[230px] h-11 p-4 rounded-[10px] border-2 justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}
|
||||||
|
>
|
||||||
|
<div className="text-center text-sm font-bold">My team</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{selectedOption === "just_me" && (
|
||||||
|
<JustMe
|
||||||
|
setSingleUserPasswordValid={setSingleUserPasswordValid}
|
||||||
|
enablePassword={enablePassword}
|
||||||
|
setEnablePassword={setEnablePassword}
|
||||||
|
justMeSubmitRef={justMeSubmitRef}
|
||||||
|
navigate={navigate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{selectedOption === "my_team" && (
|
||||||
|
<MyTeam
|
||||||
|
setMultiUserLoginValid={setMultiUserLoginValid}
|
||||||
|
myTeamSubmitRef={myTeamSubmitRef}
|
||||||
|
navigate={navigate}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const JustMe = ({
|
||||||
|
setSingleUserPasswordValid,
|
||||||
|
enablePassword,
|
||||||
|
setEnablePassword,
|
||||||
|
justMeSubmitRef,
|
||||||
|
navigate,
|
||||||
|
}) => {
|
||||||
|
const [itemSelected, setItemSelected] = useState(false);
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const { error } = await System.updateSystemPassword({
|
||||||
|
usePassword: true,
|
||||||
|
newPassword: formData.get("password"),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
showToast(`Failed to set password: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Password set successfully!", "success", { clear: true });
|
||||||
|
|
||||||
|
// Auto-request token with password that was just set so they
|
||||||
|
// are not redirected to login after completion.
|
||||||
|
const { token } = await System.requestToken({
|
||||||
|
password: formData.get("password"),
|
||||||
|
});
|
||||||
|
window.localStorage.removeItem(AUTH_USER);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||||
|
|
||||||
|
navigate(paths.onboarding.dataHandling());
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNewPassword = (e) => setPassword(e.target.value);
|
||||||
|
const handlePasswordChange = debounce(setNewPassword, 500);
|
||||||
|
|
||||||
|
function handleYes() {
|
||||||
|
setItemSelected(true);
|
||||||
|
setEnablePassword(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleNo() {
|
||||||
|
setItemSelected(true);
|
||||||
|
setEnablePassword(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (enablePassword && itemSelected && password.length >= 8) {
|
||||||
|
setSingleUserPasswordValid(true);
|
||||||
|
} else if (!enablePassword && itemSelected) {
|
||||||
|
setSingleUserPasswordValid(true);
|
||||||
|
} else {
|
||||||
|
setSingleUserPasswordValid(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (
|
||||||
|
<div className="w-full flex items-center justify-center flex-col gap-y-6">
|
||||||
|
<div className="flex flex-col border rounded-lg border-white/20 p-8 items-center gap-y-4 w-full max-w-[600px]">
|
||||||
|
<div className=" text-white text-sm font-semibold md:-ml-56">
|
||||||
|
Would you like to set up a password?
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col md:flex-row gap-6 w-full justify-center">
|
||||||
|
<button
|
||||||
|
onClick={handleYes}
|
||||||
|
className={`${
|
||||||
|
enablePassword && itemSelected
|
||||||
|
? "text-sky-400 border-sky-400/70"
|
||||||
|
: "text-white border-white/40"
|
||||||
|
} min-w-[230px] h-11 p-4 rounded-[10px] border-2 justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}
|
||||||
|
>
|
||||||
|
<div className="text-center text-sm font-bold">Yes</div>
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
onClick={handleNo}
|
||||||
|
className={`${
|
||||||
|
!enablePassword && itemSelected
|
||||||
|
? "text-sky-400 border-sky-400/70"
|
||||||
|
: "text-white border-white/40"
|
||||||
|
} min-w-[230px] h-11 p-4 rounded-[10px] border-2 justify-center items-center gap-[100px] inline-flex hover:border-sky-400/70 hover:text-sky-400 transition-all duration-300`}
|
||||||
|
>
|
||||||
|
<div className="text-center text-sm font-bold">No</div>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{enablePassword && (
|
||||||
|
<form className="w-full mt-4" onSubmit={handleSubmit}>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-3 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Instance Password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
placeholder="Your admin password"
|
||||||
|
minLength={6}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
/>
|
||||||
|
<div className="mt-4 text-white text-opacity-80 text-xs font-base -mb-2">
|
||||||
|
Passwords must be at least 8 characters.
|
||||||
|
<br />
|
||||||
|
<i>
|
||||||
|
It's important to save this password because there is no
|
||||||
|
recovery method.
|
||||||
|
</i>{" "}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={justMeSubmitRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const MyTeam = ({ setMultiUserLoginValid, myTeamSubmitRef, navigate }) => {
|
||||||
|
const [username, setUsername] = useState("");
|
||||||
|
const [password, setPassword] = useState("");
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const formData = new FormData(form);
|
||||||
|
const data = {
|
||||||
|
username: formData.get("username"),
|
||||||
|
password: formData.get("password"),
|
||||||
|
};
|
||||||
|
const { success, error } = await System.setupMultiUser(data);
|
||||||
|
if (!success) {
|
||||||
|
showToast(`Error: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast("Multi-user login enabled.", "success", { clear: true });
|
||||||
|
navigate(paths.onboarding.dataHandling());
|
||||||
|
|
||||||
|
// Auto-request token with credentials that was just set so they
|
||||||
|
// are not redirected to login after completion.
|
||||||
|
const { user, token } = await System.requestToken(data);
|
||||||
|
window.localStorage.setItem(AUTH_USER, JSON.stringify(user));
|
||||||
|
window.localStorage.setItem(AUTH_TOKEN, token);
|
||||||
|
window.localStorage.removeItem(AUTH_TIMESTAMP);
|
||||||
|
};
|
||||||
|
|
||||||
|
const setNewUsername = (e) => setUsername(e.target.value);
|
||||||
|
const setNewPassword = (e) => setPassword(e.target.value);
|
||||||
|
const handleUsernameChange = debounce(setNewUsername, 500);
|
||||||
|
const handlePasswordChange = debounce(setNewPassword, 500);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (username.length >= 6 && password.length >= 8) {
|
||||||
|
setMultiUserLoginValid(true);
|
||||||
|
} else {
|
||||||
|
setMultiUserLoginValid(false);
|
||||||
|
}
|
||||||
|
}, [username, password]);
|
||||||
|
return (
|
||||||
|
<div className="w-full flex items-center justify-center border max-w-[600px] rounded-lg border-white/20">
|
||||||
|
<form onSubmit={handleSubmit}>
|
||||||
|
<div className="flex flex-col w-full md:px-8 px-2 py-4">
|
||||||
|
<div className="space-y-6 flex h-full w-full">
|
||||||
|
<div className="w-full flex flex-col gap-y-4">
|
||||||
|
<div>
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-3 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Admin account username
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="username"
|
||||||
|
type="text"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
placeholder="Your admin username"
|
||||||
|
minLength={6}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handleUsernameChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4">
|
||||||
|
<label
|
||||||
|
htmlFor="name"
|
||||||
|
className="block mb-3 text-sm font-medium text-white"
|
||||||
|
>
|
||||||
|
Admin account password
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
name="password"
|
||||||
|
type="password"
|
||||||
|
className="bg-zinc-900 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
placeholder="Your admin password"
|
||||||
|
minLength={8}
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={handlePasswordChange}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<p className="w-96 text-white text-opacity-80 text-xs font-base">
|
||||||
|
Username must be at least 6 characters long. Password must be at
|
||||||
|
least 8 characters long.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-between items-center px-6 py-4 space-x-6 border-t rounded-b border-gray-500/50">
|
||||||
|
<div className=" text-white text-opacity-80 text-xs font-base">
|
||||||
|
By default, you will be the only admin. Once onboarding is completed
|
||||||
|
you can create and invite others to be users or admins. Do not lose
|
||||||
|
your password as only admins can reset passwords.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={myTeamSubmitRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,37 @@
|
|||||||
|
export default function VectorDatabaseItem({
|
||||||
|
name,
|
||||||
|
value,
|
||||||
|
image,
|
||||||
|
description,
|
||||||
|
checked,
|
||||||
|
onClick,
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
onClick={() => onClick(value)}
|
||||||
|
className={`w-full hover:bg-white/10 p-2 rounded-md hover:cursor-pointer ${
|
||||||
|
checked ? "bg-white/10" : ""
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
value={value}
|
||||||
|
className="peer hidden"
|
||||||
|
checked={checked}
|
||||||
|
readOnly={true}
|
||||||
|
formNoValidate={true}
|
||||||
|
/>
|
||||||
|
<div className="flex gap-x-4 items-center">
|
||||||
|
<img
|
||||||
|
src={image}
|
||||||
|
alt={`${name} logo`}
|
||||||
|
className="w-10 h-10 rounded-md"
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col gap-y-1">
|
||||||
|
<div className="text-sm font-semibold">{name}</div>
|
||||||
|
<div className="text-xs text-white tracking-wide">{description}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -0,0 +1,182 @@
|
|||||||
|
import React, { useEffect, useState, useRef } from "react";
|
||||||
|
import { MagnifyingGlass } from "@phosphor-icons/react";
|
||||||
|
import ChromaLogo from "@/media/vectordbs/chroma.png";
|
||||||
|
import PineconeLogo from "@/media/vectordbs/pinecone.png";
|
||||||
|
import LanceDbLogo from "@/media/vectordbs/lancedb.png";
|
||||||
|
import WeaviateLogo from "@/media/vectordbs/weaviate.png";
|
||||||
|
import QDrantLogo from "@/media/vectordbs/qdrant.png";
|
||||||
|
import System from "@/models/system";
|
||||||
|
import VectorDatabaseItem from "./VectorDatabaseItem";
|
||||||
|
import paths from "@/utils/paths";
|
||||||
|
import PineconeDBOptions from "@/components/VectorDBSelection/PineconeDBOptions";
|
||||||
|
import ChromaDBOptions from "@/components/VectorDBSelection/ChromaDBOptions";
|
||||||
|
import QDrantDBOptions from "@/components/VectorDBSelection/QDrantDBOptions";
|
||||||
|
import WeaviateDBOptions from "@/components/VectorDBSelection/WeaviateDBOptions";
|
||||||
|
import LanceDBOptions from "@/components/VectorDBSelection/LanceDBOptions";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import { useNavigate } from "react-router-dom";
|
||||||
|
|
||||||
|
const TITLE = "Vector Database Connection";
|
||||||
|
const DESCRIPTION =
|
||||||
|
"These are the credentials and settings for your vector database of choice.";
|
||||||
|
|
||||||
|
export default function VectorDatabaseConnection({
|
||||||
|
setHeader,
|
||||||
|
setForwardBtn,
|
||||||
|
setBackBtn,
|
||||||
|
}) {
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
const [filteredVDBs, setFilteredVDBs] = useState([]);
|
||||||
|
const [selectedVDB, setSelectedVDB] = useState(null);
|
||||||
|
const [settings, setSettings] = useState(null);
|
||||||
|
const formRef = useRef(null);
|
||||||
|
const hiddenSubmitButtonRef = useRef(null);
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchKeys() {
|
||||||
|
const _settings = await System.keys();
|
||||||
|
setSettings(_settings);
|
||||||
|
setSelectedVDB(_settings?.VectorDB || "lancedb");
|
||||||
|
}
|
||||||
|
fetchKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const VECTOR_DBS = [
|
||||||
|
{
|
||||||
|
name: "LanceDB",
|
||||||
|
value: "lancedb",
|
||||||
|
logo: LanceDbLogo,
|
||||||
|
options: <LanceDBOptions />,
|
||||||
|
description:
|
||||||
|
"100% local vector DB that runs on the same instance as AnythingLLM.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Chroma",
|
||||||
|
value: "chroma",
|
||||||
|
logo: ChromaLogo,
|
||||||
|
options: <ChromaDBOptions settings={settings} />,
|
||||||
|
description:
|
||||||
|
"Open source vector database you can host yourself or on the cloud.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Pinecone",
|
||||||
|
value: "pinecone",
|
||||||
|
logo: PineconeLogo,
|
||||||
|
options: <PineconeDBOptions settings={settings} />,
|
||||||
|
description: "100% cloud-based vector database for enterprise use cases.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "QDrant",
|
||||||
|
value: "qdrant",
|
||||||
|
logo: QDrantLogo,
|
||||||
|
options: <QDrantDBOptions settings={settings} />,
|
||||||
|
description: "Open source local and distributed cloud vector database.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "Weaviate",
|
||||||
|
value: "weaviate",
|
||||||
|
logo: WeaviateLogo,
|
||||||
|
options: <WeaviateDBOptions settings={settings} />,
|
||||||
|
description:
|
||||||
|
"Open source local and cloud hosted multi-modal vector database.",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
function handleForward() {
|
||||||
|
if (hiddenSubmitButtonRef.current) {
|
||||||
|
hiddenSubmitButtonRef.current.click();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBack() {
|
||||||
|
navigate(paths.onboarding.embeddingPreference());
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = e.target;
|
||||||
|
const data = {};
|
||||||
|
const formData = new FormData(form);
|
||||||
|
data.VectorDB = selectedVDB;
|
||||||
|
for (var [key, value] of formData.entries()) data[key] = value;
|
||||||
|
const { error } = await System.updateSystem(data);
|
||||||
|
if (error) {
|
||||||
|
showToast(`Failed to save Vector Database settings: ${error}`, "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
showToast("Vector Database settings saved successfully.", "success", {
|
||||||
|
clear: true,
|
||||||
|
});
|
||||||
|
navigate(paths.onboarding.customLogo());
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHeader({ title: TITLE, description: DESCRIPTION });
|
||||||
|
setForwardBtn({ showing: true, disabled: false, onClick: handleForward });
|
||||||
|
setBackBtn({ showing: true, disabled: false, onClick: handleBack });
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (searchQuery.trim() === "") {
|
||||||
|
setFilteredVDBs(VECTOR_DBS);
|
||||||
|
} else {
|
||||||
|
const lowercasedQuery = searchQuery.toLowerCase();
|
||||||
|
const filtered = VECTOR_DBS.filter((vdb) =>
|
||||||
|
vdb.name.toLowerCase().includes(lowercasedQuery)
|
||||||
|
);
|
||||||
|
setFilteredVDBs(filtered);
|
||||||
|
}
|
||||||
|
}, [searchQuery]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<form ref={formRef} onSubmit={handleSubmit} className="w-full">
|
||||||
|
<div className="w-full relative border-slate-300/40 shadow border-2 rounded-lg text-white pb-4">
|
||||||
|
<div className="w-full p-4 absolute top-0 rounded-t-lg bg-accent/50">
|
||||||
|
<div className="w-full flex items-center sticky top-0 z-20">
|
||||||
|
<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 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">
|
||||||
|
{filteredVDBs.map((vdb) => (
|
||||||
|
<VectorDatabaseItem
|
||||||
|
key={vdb.name}
|
||||||
|
name={vdb.name}
|
||||||
|
value={vdb.value}
|
||||||
|
image={vdb.logo}
|
||||||
|
description={vdb.description}
|
||||||
|
checked={selectedVDB === vdb.value}
|
||||||
|
onClick={setSelectedVDB}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-4 flex flex-col gap-y-1">
|
||||||
|
{selectedVDB &&
|
||||||
|
VECTOR_DBS.find((vdb) => vdb.value === selectedVDB)?.options}
|
||||||
|
</div>
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
ref={hiddenSubmitButtonRef}
|
||||||
|
hidden
|
||||||
|
aria-hidden="true"
|
||||||
|
></button>
|
||||||
|
</form>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
130
frontend/src/pages/OnboardingFlow/Steps/index.jsx
Normal file
130
frontend/src/pages/OnboardingFlow/Steps/index.jsx
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
import { ArrowLeft, ArrowRight } from "@phosphor-icons/react";
|
||||||
|
import { lazy, useState } from "react";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
const OnboardingSteps = {
|
||||||
|
home: lazy(() => import("./Home")),
|
||||||
|
"llm-preference": lazy(() => import("./LLMPreference")),
|
||||||
|
"embedding-preference": lazy(() => import("./EmbeddingPreference")),
|
||||||
|
"vector-database": lazy(() => import("./VectorDatabaseConnection")),
|
||||||
|
"custom-logo": lazy(() => import("./CustomLogo")),
|
||||||
|
"user-setup": lazy(() => import("./UserSetup")),
|
||||||
|
"data-handling": lazy(() => import("./DataHandling")),
|
||||||
|
survey: lazy(() => import("./Survey")),
|
||||||
|
"create-workspace": lazy(() => import("./CreateWorkspace")),
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OnboardingSteps;
|
||||||
|
|
||||||
|
export function OnboardingLayout({ children }) {
|
||||||
|
const [header, setHeader] = useState({
|
||||||
|
title: "",
|
||||||
|
description: "",
|
||||||
|
});
|
||||||
|
const [backBtn, setBackBtn] = useState({
|
||||||
|
showing: false,
|
||||||
|
disabled: true,
|
||||||
|
onClick: () => null,
|
||||||
|
});
|
||||||
|
const [forwardBtn, setForwardBtn] = useState({
|
||||||
|
showing: false,
|
||||||
|
disabled: true,
|
||||||
|
onClick: () => null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (isMobile) {
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-y-auto bg-[#2C2F35] overflow-hidden">
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<div className="w-full relative py-10 px-2">
|
||||||
|
<div className="flex flex-col w-fit mx-auto gap-y-1 mb-[55px]">
|
||||||
|
<h1 className="text-white font-semibold text-center text-2xl">
|
||||||
|
{header.title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-zinc-400 text-base text-center">
|
||||||
|
{header.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{children(setHeader, setBackBtn, setForwardBtn)}
|
||||||
|
</div>
|
||||||
|
<div className="flex w-full justify-center gap-x-4 pb-20">
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
{backBtn.showing && (
|
||||||
|
<button
|
||||||
|
disabled={backBtn.disabled}
|
||||||
|
onClick={backBtn.onClick}
|
||||||
|
className="group p-2 rounded-lg border-2 border-zinc-300 disabled:border-zinc-600 h-fit w-fit disabled:not-allowed hover:bg-zinc-100 disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<ArrowLeft
|
||||||
|
className="text-white group-hover:text-black group-disabled:text-gray-500"
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-center items-center">
|
||||||
|
{forwardBtn.showing && (
|
||||||
|
<button
|
||||||
|
disabled={forwardBtn.disabled}
|
||||||
|
onClick={forwardBtn.onClick}
|
||||||
|
className="group p-2 rounded-lg border-2 border-zinc-300 disabled:border-zinc-600 h-fit w-fit disabled:not-allowed hover:bg-zinc-100 disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<ArrowRight
|
||||||
|
className="text-white group-hover:text-black group-disabled:text-gray-500"
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen overflow-y-auto bg-[#2C2F35] md:bg-main-gradient flex justify-center overflow-hidden">
|
||||||
|
<div className="flex w-1/5 h-screen justify-center items-center">
|
||||||
|
{backBtn.showing && (
|
||||||
|
<button
|
||||||
|
disabled={backBtn.disabled}
|
||||||
|
onClick={backBtn.onClick}
|
||||||
|
className="group p-2 rounded-lg border-2 border-zinc-300 disabled:border-zinc-600 h-fit w-fit disabled:not-allowed hover:bg-zinc-100 disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<ArrowLeft
|
||||||
|
className="text-white group-hover:text-black group-disabled:text-gray-500"
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="w-full md:w-3/5 relative h-full py-10">
|
||||||
|
<div className="flex flex-col w-fit mx-auto gap-y-1 mb-[55px]">
|
||||||
|
<h1 className="text-white font-semibold text-center text-2xl">
|
||||||
|
{header.title}
|
||||||
|
</h1>
|
||||||
|
<p className="text-zinc-400 text-base text-center">
|
||||||
|
{header.description}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{children(setHeader, setBackBtn, setForwardBtn)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex w-1/5 h-screen justify-center items-center">
|
||||||
|
{forwardBtn.showing && (
|
||||||
|
<button
|
||||||
|
disabled={forwardBtn.disabled}
|
||||||
|
onClick={forwardBtn.onClick}
|
||||||
|
className="group p-2 rounded-lg border-2 border-zinc-300 disabled:border-zinc-600 h-fit w-fit disabled:not-allowed hover:bg-zinc-100 disabled:hover:bg-transparent"
|
||||||
|
>
|
||||||
|
<ArrowRight
|
||||||
|
className="text-white group-hover:text-black group-disabled:text-gray-500"
|
||||||
|
size={30}
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
@ -1,57 +1,21 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React from "react";
|
||||||
import OnboardingModal, { OnboardingModalId } from "./OnboardingModal";
|
import OnboardingSteps, { OnboardingLayout } from "./Steps";
|
||||||
import useLogo from "@/hooks/useLogo";
|
import { useParams } from "react-router-dom";
|
||||||
import { isMobile } from "react-device-detect";
|
|
||||||
|
|
||||||
export default function OnboardingFlow() {
|
export default function OnboardingFlow() {
|
||||||
const { logo } = useLogo();
|
const { step } = useParams();
|
||||||
const [modalVisible, setModalVisible] = useState(false);
|
const StepPage = OnboardingSteps[step || "home"];
|
||||||
|
if (step === "home" || !step) return <StepPage />;
|
||||||
useEffect(() => {
|
|
||||||
if (modalVisible) {
|
|
||||||
document.getElementById(OnboardingModalId)?.showModal();
|
|
||||||
}
|
|
||||||
}, [modalVisible]);
|
|
||||||
|
|
||||||
function showModal() {
|
|
||||||
setModalVisible(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isMobile) {
|
|
||||||
return (
|
|
||||||
<div className="w-screen h-full bg-sidebar flex items-center justify-center">
|
|
||||||
<div className="w-fit p-20 py-24 border-2 border-slate-300/10 rounded-2xl bg-main-gradient shadow-lg">
|
|
||||||
<div className="text-white text-2xl font-base text-center">
|
|
||||||
Welcome to
|
|
||||||
</div>
|
|
||||||
<img src={logo} alt="logo" className="w-80 mx-auto m-3 mb-11" />
|
|
||||||
<div className="flex justify-center items-center">
|
|
||||||
<p className="text-white text-sm italic text-center">
|
|
||||||
Please use a desktop browser to continue onboarding.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-screen h-full bg-sidebar flex items-center justify-center">
|
<OnboardingLayout>
|
||||||
<div className="w-fit p-20 py-24 border-2 border-slate-300/10 rounded-2xl bg-main-gradient shadow-lg">
|
{(setHeader, setBackBtn, setForwardBtn) => (
|
||||||
<div className="text-white text-2xl font-base text-center">
|
<StepPage
|
||||||
Welcome to
|
setHeader={setHeader}
|
||||||
</div>
|
setBackBtn={setBackBtn}
|
||||||
<img src={logo} alt="logo" className="w-80 mx-auto m-3 mb-11" />
|
setForwardBtn={setForwardBtn}
|
||||||
<div className="flex justify-center items-center">
|
/>
|
||||||
<button
|
)}
|
||||||
className="border border-slate-200 px-5 py-2.5 rounded-lg text-slate-800 bg-slate-200 text-sm items-center flex gap-x-2 hover:text-white hover:bg-transparent focus:ring-gray-800 font-semibold shadow animate-pulse"
|
</OnboardingLayout>
|
||||||
onClick={showModal}
|
|
||||||
>
|
|
||||||
Get Started
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{modalVisible && <OnboardingModal setModalVisible={setModalVisible} />}
|
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -7,9 +7,35 @@ export default {
|
|||||||
login: () => {
|
login: () => {
|
||||||
return "/login";
|
return "/login";
|
||||||
},
|
},
|
||||||
onboarding: () => {
|
onboarding: {
|
||||||
|
home: () => {
|
||||||
return "/onboarding";
|
return "/onboarding";
|
||||||
},
|
},
|
||||||
|
survey: () => {
|
||||||
|
return "/onboarding/survey";
|
||||||
|
},
|
||||||
|
llmPreference: () => {
|
||||||
|
return "/onboarding/llm-preference";
|
||||||
|
},
|
||||||
|
embeddingPreference: () => {
|
||||||
|
return "/onboarding/embedding-preference";
|
||||||
|
},
|
||||||
|
vectorDatabase: () => {
|
||||||
|
return "/onboarding/vector-database";
|
||||||
|
},
|
||||||
|
customLogo: () => {
|
||||||
|
return "/onboarding/custom-logo";
|
||||||
|
},
|
||||||
|
userSetup: () => {
|
||||||
|
return "/onboarding/user-setup";
|
||||||
|
},
|
||||||
|
dataHandling: () => {
|
||||||
|
return "/onboarding/data-handling";
|
||||||
|
},
|
||||||
|
createWorkspace: () => {
|
||||||
|
return "/onboarding/create-workspace";
|
||||||
|
},
|
||||||
|
},
|
||||||
github: () => {
|
github: () => {
|
||||||
return "https://github.com/Mintplex-Labs/anything-llm";
|
return "https://github.com/Mintplex-Labs/anything-llm";
|
||||||
},
|
},
|
||||||
|
@ -1,12 +1,13 @@
|
|||||||
const { PrismaClient } = require('@prisma/client');
|
const { PrismaClient } = require("@prisma/client");
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
|
|
||||||
async function main() {
|
async function main() {
|
||||||
const settings = [
|
const settings = [
|
||||||
{ label: 'multi_user_mode', value: 'false' },
|
{ label: "multi_user_mode", value: "false" },
|
||||||
{ label: 'users_can_delete_workspaces', value: 'false' },
|
{ label: "users_can_delete_workspaces", value: "false" },
|
||||||
{ label: 'limit_user_messages', value: 'false' },
|
{ label: "limit_user_messages", value: "false" },
|
||||||
{ label: 'message_limit', value: '25' },
|
{ label: "message_limit", value: "25" },
|
||||||
|
{ label: "logo_filename", value: "anything-llm.png" },
|
||||||
];
|
];
|
||||||
|
|
||||||
for (let setting of settings) {
|
for (let setting of settings) {
|
||||||
@ -24,7 +25,7 @@ async function main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
main()
|
main()
|
||||||
.catch(e => {
|
.catch((e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user