diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 7224f2e9c03ddd53e146dcc8d61ceb955b823ff3..8007b5ad1304cca10313278167b557e025489ccf 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -120,6 +120,7 @@ export default function App() { {/* Onboarding Flow */} <Route path="/onboarding" element={<OnboardingFlow />} /> + <Route path="/onboarding/:step" element={<OnboardingFlow />} /> </Routes> <ToastContainer /> </PfpProvider> diff --git a/frontend/src/components/EmbeddingSelection/AzureAiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/AzureAiOptions/index.jsx index e7767900aec6e90124fa660fb1090a25632bd2ec..c782c51f334c48595e89408aa32923480b9fbfbe 100644 --- a/frontend/src/components/EmbeddingSelection/AzureAiOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/AzureAiOptions/index.jsx @@ -1,53 +1,55 @@ export default function AzureAiOptions({ settings }) { return ( - <> - <div className="flex flex-col w-60"> - <label className="text-white text-sm font-semibold block mb-4"> - Azure Service Endpoint - </label> - <input - type="url" - name="AzureOpenAiEndpoint" - 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="https://my-azure.openai.azure.com" - defaultValue={settings?.AzureOpenAiEndpoint} - required={true} - autoComplete="off" - spellCheck={false} - /> - </div> + <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"> + Azure Service Endpoint + </label> + <input + type="url" + name="AzureOpenAiEndpoint" + 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="https://my-azure.openai.azure.com" + defaultValue={settings?.AzureOpenAiEndpoint} + 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="AzureOpenAiKey" - 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="Azure OpenAI API Key" - defaultValue={settings?.AzureOpenAiKey ? "*".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"> + API Key + </label> + <input + type="password" + name="AzureOpenAiKey" + 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="Azure OpenAI API Key" + defaultValue={settings?.AzureOpenAiKey ? "*".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"> - Embedding Deployment Name - </label> - <input - type="text" - name="AzureOpenAiEmbeddingModelPref" - 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="Azure OpenAI embedding model deployment name" - defaultValue={settings?.AzureOpenAiEmbeddingModelPref} - required={true} - autoComplete="off" - spellCheck={false} - /> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Embedding Deployment Name + </label> + <input + type="text" + name="AzureOpenAiEmbeddingModelPref" + 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="Azure OpenAI embedding model deployment name" + defaultValue={settings?.AzureOpenAiEmbeddingModelPref} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> </div> - </> + </div> ); } diff --git a/frontend/src/components/EmbeddingSelection/LocalAiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/LocalAiOptions/index.jsx index 2b976e1428b2ea995f527843e16bf8a00129f8ab..6f81712c14bd2578b95e651fb0b05ee4cc7f8d55 100644 --- a/frontend/src/components/EmbeddingSelection/LocalAiOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/LocalAiOptions/index.jsx @@ -10,72 +10,72 @@ export default function LocalAiOptions({ settings }) { const [apiKey, setApiKey] = useState(settings?.LocalAiApiKey); 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"> - LocalAI Base URL - </label> - <input - type="url" - name="EmbeddingBasePath" - 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/v1" - defaultValue={settings?.EmbeddingBasePath} - onChange={(e) => setBasePathValue(e.target.value)} - onBlur={() => setBasePath(basePathValue)} - required={true} - autoComplete="off" - spellCheck={false} - /> - </div> - <LocalAIModelSelection - settings={settings} - apiKey={apiKey} - basePath={basePath} - /> - <div className="flex flex-col w-60"> - <label className="text-white text-sm font-semibold block mb-4"> - Max embedding chunk length - </label> - <input - type="number" - name="EmbeddingModelMaxChunkLength" - 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="1000" - min={1} - onScroll={(e) => e.target.blur()} - defaultValue={settings?.EmbeddingModelMaxChunkLength} - required={false} - autoComplete="off" + <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"> + LocalAI Base URL + </label> + <input + type="url" + name="EmbeddingBasePath" + 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/v1" + defaultValue={settings?.EmbeddingBasePath} + onChange={(e) => setBasePathValue(e.target.value)} + onBlur={() => setBasePath(basePathValue)} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> + <LocalAIModelSelection + settings={settings} + apiKey={apiKey} + basePath={basePath} /> - </div> - </div> - <div className="w-full flex items-center gap-4"> - <div className="flex flex-col w-60"> - <div className="flex flex-col gap-y-1 mb-4"> - <label className="text-white text-sm font-semibold block"> - Local AI API Key + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Max embedding chunk length </label> - <p className="text-xs italic text-white/60"> - optional API key to use if running LocalAI with API keys. - </p> + <input + type="number" + name="EmbeddingModelMaxChunkLength" + 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="1000" + min={1} + onScroll={(e) => e.target.blur()} + defaultValue={settings?.EmbeddingModelMaxChunkLength} + required={false} + autoComplete="off" + /> </div> + </div> + <div className="w-full flex items-center gap-4"> + <div className="flex flex-col w-60"> + <div className="flex flex-col gap-y-1 mb-4"> + <label className="text-white text-sm font-semibold flex items-center gap-x-2"> + Local AI API Key{" "} + <p className="!text-xs !italic !font-thin">optional</p> + </label> + </div> - <input - type="password" - name="LocalAiApiKey" - 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-mysecretkey" - defaultValue={settings?.LocalAiApiKey ? "*".repeat(20) : ""} - autoComplete="off" - spellCheck={false} - onChange={(e) => setApiKeyValue(e.target.value)} - onBlur={() => setApiKey(apiKeyValue)} - /> + <input + type="password" + name="LocalAiApiKey" + 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-mysecretkey" + defaultValue={settings?.LocalAiApiKey ? "*".repeat(20) : ""} + autoComplete="off" + spellCheck={false} + onChange={(e) => setApiKeyValue(e.target.value)} + onBlur={() => setApiKey(apiKeyValue)} + /> + </div> </div> </div> - </> + </div> ); } diff --git a/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx b/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx index f38f7c445ee2ea0d8a63ed1eac5c3c752efbfa2b..dd00d67abdd564b9e49c0177546323e35d9d3d7d 100644 --- a/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx +++ b/frontend/src/components/EmbeddingSelection/OpenAiOptions/index.jsx @@ -1,34 +1,36 @@ export default function OpenAiOptions({ settings }) { return ( - <> - <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="OpenAiKey" - 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="OpenAI API Key" - defaultValue={settings?.OpenAiKey ? "*".repeat(20) : ""} - required={true} - autoComplete="off" - spellCheck={false} - /> + <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"> + API Key + </label> + <input + type="password" + name="OpenAiKey" + 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="OpenAI API Key" + defaultValue={settings?.OpenAiKey ? "*".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"> + Model Preference + </label> + <select + disabled={true} + className="cursor-not-allowed bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5" + > + <option disabled={true} selected={true}> + text-embedding-ada-002 + </option> + </select> + </div> </div> - <div className="flex flex-col w-60"> - <label className="text-white text-sm font-semibold block mb-4"> - Model Preference - </label> - <select - disabled={true} - className="cursor-not-allowed bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5" - > - <option disabled={true} selected={true}> - text-embedding-ada-002 - </option> - </select> - </div> - </> + </div> ); } diff --git a/frontend/src/components/LLMSelection/AzureAiOptions/index.jsx b/frontend/src/components/LLMSelection/AzureAiOptions/index.jsx index 2978651016c9baa5d9db83ac81cbb62bed969325..ce54d3d60dea414c77446284469643c9f6c76fd5 100644 --- a/frontend/src/components/LLMSelection/AzureAiOptions/index.jsx +++ b/frontend/src/components/LLMSelection/AzureAiOptions/index.jsx @@ -1,87 +1,92 @@ export default function AzureAiOptions({ settings }) { return ( - <> - <div className="flex flex-col w-60"> - <label className="text-white text-sm font-semibold block mb-4"> - Azure Service Endpoint - </label> - <input - type="url" - name="AzureOpenAiEndpoint" - 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="https://my-azure.openai.azure.com" - defaultValue={settings?.AzureOpenAiEndpoint} - required={true} - autoComplete="off" - spellCheck={false} - /> - </div> + <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"> + Azure Service Endpoint + </label> + <input + type="url" + name="AzureOpenAiEndpoint" + 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="https://my-azure.openai.azure.com" + defaultValue={settings?.AzureOpenAiEndpoint} + 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="AzureOpenAiKey" - 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="Azure OpenAI API Key" - defaultValue={settings?.AzureOpenAiKey ? "*".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"> + API Key + </label> + <input + type="password" + name="AzureOpenAiKey" + 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="Azure OpenAI API Key" + defaultValue={settings?.AzureOpenAiKey ? "*".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"> - Chat Deployment Name - </label> - <input - type="text" - name="AzureOpenAiModelPref" - 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="Azure OpenAI chat model deployment name" - defaultValue={settings?.AzureOpenAiModelPref} - required={true} - autoComplete="off" - spellCheck={false} - /> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Chat Deployment Name + </label> + <input + type="text" + name="AzureOpenAiModelPref" + 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="Azure OpenAI chat model deployment name" + defaultValue={settings?.AzureOpenAiModelPref} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> </div> - <div className="flex flex-col w-60"> - <label className="text-white text-sm font-semibold block mb-4"> - Chat Model Token Limit - </label> - <select - name="AzureOpenAiTokenLimit" - defaultValue={settings?.AzureOpenAiTokenLimit || 4096} - className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5" - required={true} - > - <option value={4096}>4,096 (gpt-3.5-turbo)</option> - <option value={16384}>16,384 (gpt-3.5-16k)</option> - <option value={8192}>8,192 (gpt-4)</option> - <option value={32768}>32,768 (gpt-4-32k)</option> - <option value={128000}>128,000 (gpt-4-turbo)</option> - </select> - </div> + <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"> + Chat Model Token Limit + </label> + <select + name="AzureOpenAiTokenLimit" + defaultValue={settings?.AzureOpenAiTokenLimit || 4096} + className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5" + required={true} + > + <option value={4096}>4,096 (gpt-3.5-turbo)</option> + <option value={16384}>16,384 (gpt-3.5-16k)</option> + <option value={8192}>8,192 (gpt-4)</option> + <option value={32768}>32,768 (gpt-4-32k)</option> + <option value={128000}>128,000 (gpt-4-turbo)</option> + </select> + </div> - <div className="flex flex-col w-60"> - <label className="text-white text-sm font-semibold block mb-4"> - Embedding Deployment Name - </label> - <input - type="text" - name="AzureOpenAiEmbeddingModelPref" - 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="Azure OpenAI embedding model deployment name" - defaultValue={settings?.AzureOpenAiEmbeddingModelPref} - required={true} - autoComplete="off" - spellCheck={false} - /> + <div className="flex flex-col w-60"> + <label className="text-white text-sm font-semibold block mb-4"> + Embedding Deployment Name + </label> + <input + type="text" + name="AzureOpenAiEmbeddingModelPref" + 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="Azure OpenAI embedding model deployment name" + defaultValue={settings?.AzureOpenAiEmbeddingModelPref} + required={true} + autoComplete="off" + spellCheck={false} + /> + </div> + <div className="flex-flex-col w-60"></div> </div> - </> + </div> ); } diff --git a/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx b/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx index fd2f876651b505f647966f5c2331166a1734ac84..cbd83edb99aa85b5934d5d55e0229d5ad4c5824f 100644 --- a/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx +++ b/frontend/src/components/LLMSelection/OpenAiOptions/index.jsx @@ -6,7 +6,7 @@ export default function OpenAiOptions({ settings }) { const [openAIKey, setOpenAIKey] = useState(settings?.OpenAiKey); return ( - <> + <div className="flex gap-x-4"> <div className="flex flex-col w-60"> <label className="text-white text-sm font-semibold block mb-4"> API Key @@ -25,7 +25,7 @@ export default function OpenAiOptions({ settings }) { /> </div> <OpenAIModelSelection settings={settings} apiKey={openAIKey} /> - </> + </div> ); } @@ -87,7 +87,7 @@ function OpenAIModelSelection({ apiKey, settings }) { <option key={model} value={model} - selected={settings.OpenAiModelPref === model} + selected={settings?.OpenAiModelPref === model} > {model} </option> @@ -102,7 +102,7 @@ function OpenAIModelSelection({ apiKey, settings }) { <option key={model.id} value={model.id} - selected={settings.OpenAiModelPref === model.id} + selected={settings?.OpenAiModelPref === model.id} > {model.id} </option> diff --git a/frontend/src/components/PrivateRoute/index.jsx b/frontend/src/components/PrivateRoute/index.jsx index 4640646321c87bda9413c598c8cc332ec0f73436..165141bbb3a67de6333db69b614107a7803cbf24 100644 --- a/frontend/src/components/PrivateRoute/index.jsx +++ b/frontend/src/components/PrivateRoute/index.jsx @@ -89,7 +89,7 @@ export function AdminRoute({ Component }) { if (isAuthd === null) return <FullScreenLoader />; if (shouldRedirectToOnboarding) { - return <Navigate to={paths.onboarding()} />; + return <Navigate to={paths.onboarding.home()} />; } const user = userFromStorage(); @@ -110,7 +110,7 @@ export function ManagerRoute({ Component }) { if (isAuthd === null) return <FullScreenLoader />; if (shouldRedirectToOnboarding) { - return <Navigate to={paths.onboarding()} />; + return <Navigate to={paths.onboarding.home()} />; } const user = userFromStorage(); diff --git a/frontend/src/components/VectorDBSelection/ChromaDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/ChromaDBOptions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ae7af68fb84772aeb924d40887f4b188630b5389 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/ChromaDBOptions/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..942a3666da7b78aac975a45e53fc4099b836e724 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/LanceDBOptions/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/VectorDBSelection/PineconeDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/PineconeDBOptions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5491f758c67791a36c50568e3faed1f1ac2cb54c --- /dev/null +++ b/frontend/src/components/VectorDBSelection/PineconeDBOptions/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/VectorDBSelection/QDrantDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/QDrantDBOptions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..e1e9d90f6807be87fbec9fc6ba8688bb004e8cc4 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/QDrantDBOptions/index.jsx @@ -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> + ); +} diff --git a/frontend/src/components/VectorDBSelection/WeaviateDBOptions/index.jsx b/frontend/src/components/VectorDBSelection/WeaviateDBOptions/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..5d7494ed155e831f7e21122bc350b07b78755cd6 --- /dev/null +++ b/frontend/src/components/VectorDBSelection/WeaviateDBOptions/index.jsx @@ -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> + ); +} diff --git a/frontend/src/media/illustrations/create-workspace.png b/frontend/src/media/illustrations/create-workspace.png new file mode 100644 index 0000000000000000000000000000000000000000..8e31174e5d1d7543adb75ec1ee7ec1ac6182944e Binary files /dev/null and b/frontend/src/media/illustrations/create-workspace.png differ diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/AppearanceSetup/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/AppearanceSetup/index.jsx deleted file mode 100644 index 30e87b0ac6b7420a9a18d455da3232369dea97a1..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/AppearanceSetup/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/CreateFirstWorkspace/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/CreateFirstWorkspace/index.jsx deleted file mode 100644 index d2624ef6a103324a133a8d86dbf6dd28181dcca9..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/CreateFirstWorkspace/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx deleted file mode 100644 index 98e1262a04f8993046d8bbf712be964aabae2824..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/EmbeddingSelection/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx deleted file mode 100644 index 850dea3c2ec8b86a21678b8c8fdd7403994c9c7c..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/LLMSelection/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/MultiUserSetup/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/MultiUserSetup/index.jsx deleted file mode 100644 index 71310abfcae012dfe190a9b7ea3871a1d3a0e72b..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/MultiUserSetup/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/PasswordProtection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/PasswordProtection/index.jsx deleted file mode 100644 index 4504288e6a95b82695c0737daae452b515ca9d5e..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/PasswordProtection/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserModeSelection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserModeSelection/index.jsx deleted file mode 100644 index b78c325330838dc91aa448bdde100c145792440e..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserModeSelection/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserQuestionnaire/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserQuestionnaire/index.jsx deleted file mode 100644 index a0cb97fd75c119facb73ee480fc01386e9502503..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/UserQuestionnaire/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/VectorDatabaseConnection/index.jsx deleted file mode 100644 index 16ee9aa53b3f8cdaf571b432d5e5c37bef94a35b..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/VectorDatabaseConnection/index.jsx +++ /dev/null @@ -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); diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx b/frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx deleted file mode 100644 index 9815a7e80dd1bbb7bee50827baaa6d93c22a1b8b..0000000000000000000000000000000000000000 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/index.jsx +++ /dev/null @@ -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> - ); -} diff --git a/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..aa53c87fc4a9f2f3b02f87d8c0c3f0231269c336 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/CreateWorkspace/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..bb421bc39a953981f0349723dd2bb2fb9f7eda6f --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/CustomLogo/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx similarity index 87% rename from frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx rename to frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx index 81b93c5dc0952fb96e26c30dcf727c23d5c5b311..db285f128328c08c6fa8c3f8bed879eac5099fe1 100644 --- a/frontend/src/pages/OnboardingFlow/OnboardingModal/Steps/DataHandling/index.jsx +++ b/frontend/src/pages/OnboardingFlow/Steps/DataHandling/index.jsx @@ -1,4 +1,4 @@ -import React, { memo, useEffect, useState } from "react"; +import PreLoader from "@/components/Preloader"; import System from "@/models/system"; import AnythingLLMIcon from "@/media/logo/anything-llm-icon.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 WeaviateLogo from "@/media/vectordbs/weaviate.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 = { 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 [loading, setLoading] = useState(true); const [vectorDb, setVectorDb] = useState("pinecone"); const [embeddingEngine, setEmbeddingEngine] = useState("openai"); + const navigate = useNavigate(); useEffect(() => { + setHeader({ title: TITLE, description: DESCRIPTION }); + setForwardBtn({ showing: true, disabled: false, onClick: handleForward }); + setBackBtn({ showing: false, disabled: false, onClick: handleBack }); async function fetchKeys() { const _settings = await System.keys(); - setLLMChoice(_settings?.LLMProvider); - setVectorDb(_settings?.VectorDB); - setEmbeddingEngine(_settings?.EmbeddingEngine); + setLLMChoice(_settings?.LLMProvider || "openai"); + setVectorDb(_settings?.VectorDB || "pinecone"); + setEmbeddingEngine(_settings?.EmbeddingEngine || "openai"); setLoading(false); } - if (currentStep === "data_handling") { - fetchKeys(); - } + fetchKeys(); }, []); + function handleForward() { + navigate(paths.onboarding.survey()); + } + + function handleBack() { + navigate(paths.onboarding.userSetup()); + } + if (loading) return ( <div className="w-full h-full flex justify-center items-center p-20"> @@ -179,7 +194,7 @@ function DataHandling({ nextStep, prevStep, currentStep }) { ); 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="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> @@ -239,23 +254,6 @@ function DataHandling({ nextStep, prevStep, currentStep }) { </ul> </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> ); } - -export default memo(DataHandling); diff --git a/frontend/src/pages/OnboardingFlow/Steps/EmbeddingPreference/EmbedderItem.jsx b/frontend/src/pages/OnboardingFlow/Steps/EmbeddingPreference/EmbedderItem.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b37b645f95a282c129ecf199a9cdc72b7e60c06a --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/EmbeddingPreference/EmbedderItem.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/EmbeddingPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/EmbeddingPreference/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..c78d3f4434ba08b1693e863ca8a059f00ae7226e --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/EmbeddingPreference/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7b31e2ac3db2bad94916f3cfc1740dd03e47c429 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/Home/index.jsx @@ -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> + </> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/Home/l_group.png b/frontend/src/pages/OnboardingFlow/Steps/Home/l_group.png new file mode 100644 index 0000000000000000000000000000000000000000..2981196a33208cdb5c6fa0a39b562176d42642e0 Binary files /dev/null and b/frontend/src/pages/OnboardingFlow/Steps/Home/l_group.png differ diff --git a/frontend/src/pages/OnboardingFlow/Steps/Home/r_group.png b/frontend/src/pages/OnboardingFlow/Steps/Home/r_group.png new file mode 100644 index 0000000000000000000000000000000000000000..fc50fd52c875578c55fa4e18d2143d0265ade213 Binary files /dev/null and b/frontend/src/pages/OnboardingFlow/Steps/Home/r_group.png differ diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/LLMItem.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/LLMItem.jsx new file mode 100644 index 0000000000000000000000000000000000000000..b6db5d130376b84cb097ecfb25c248b982a2318e --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/LLMItem.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..24d561ed6d8d6256c6fc831e41f94593ec6ece01 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/LLMPreference/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/Survey/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/Survey/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..35b2f67d07555f1c2286ef158d4bf011261f348a --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/Survey/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..500a483a18c6cfa738db53e406d28ce17ac4c427 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/UserSetup/index.jsx @@ -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> + ); +}; diff --git a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/VectorDatabaseItem.jsx b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/VectorDatabaseItem.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4ecd304f7a1280b1762b9a8f791c37153cbb9d0a --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/VectorDatabaseItem.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..17accbab0080a3adf69246ec168671cb7a7183e2 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/VectorDatabaseConnection/index.jsx @@ -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> + </> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/Steps/index.jsx b/frontend/src/pages/OnboardingFlow/Steps/index.jsx new file mode 100644 index 0000000000000000000000000000000000000000..3f218d531b6ed318c6364162b4b14604547cbef8 --- /dev/null +++ b/frontend/src/pages/OnboardingFlow/Steps/index.jsx @@ -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> + ); +} diff --git a/frontend/src/pages/OnboardingFlow/index.jsx b/frontend/src/pages/OnboardingFlow/index.jsx index 106f7cbae1f5d2d6b596a40748b63a6861693c90..c46b3c0bc908cafa90b3a884aa9d943d2098e7dd 100644 --- a/frontend/src/pages/OnboardingFlow/index.jsx +++ b/frontend/src/pages/OnboardingFlow/index.jsx @@ -1,57 +1,21 @@ -import React, { useEffect, useState } from "react"; -import OnboardingModal, { OnboardingModalId } from "./OnboardingModal"; -import useLogo from "@/hooks/useLogo"; -import { isMobile } from "react-device-detect"; +import React from "react"; +import OnboardingSteps, { OnboardingLayout } from "./Steps"; +import { useParams } from "react-router-dom"; export default function OnboardingFlow() { - const { logo } = useLogo(); - const [modalVisible, setModalVisible] = useState(false); - - 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> - ); - } + const { step } = useParams(); + const StepPage = OnboardingSteps[step || "home"]; + if (step === "home" || !step) return <StepPage />; 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"> - <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" - onClick={showModal} - > - Get Started - </button> - </div> - </div> - {modalVisible && <OnboardingModal setModalVisible={setModalVisible} />} - </div> + <OnboardingLayout> + {(setHeader, setBackBtn, setForwardBtn) => ( + <StepPage + setHeader={setHeader} + setBackBtn={setBackBtn} + setForwardBtn={setForwardBtn} + /> + )} + </OnboardingLayout> ); } diff --git a/frontend/src/utils/paths.js b/frontend/src/utils/paths.js index 7a0c7bb70cdf4fb48bb8668be0e639a5ba0f96dd..547a3b3f31b8950ac3a342235d08499f7681e2f2 100644 --- a/frontend/src/utils/paths.js +++ b/frontend/src/utils/paths.js @@ -7,8 +7,34 @@ export default { login: () => { return "/login"; }, - onboarding: () => { - return "/onboarding"; + onboarding: { + home: () => { + 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: () => { return "https://github.com/Mintplex-Labs/anything-llm"; diff --git a/server/prisma/seed.js b/server/prisma/seed.js index 71d1e63d6829ab2085a39d6a6d4cfb46ccdf5f02..829b812ab4eb02d8fb2ca80e3c992cb55ae16b3a 100644 --- a/server/prisma/seed.js +++ b/server/prisma/seed.js @@ -1,12 +1,13 @@ -const { PrismaClient } = require('@prisma/client'); +const { PrismaClient } = require("@prisma/client"); const prisma = new PrismaClient(); async function main() { const settings = [ - { label: 'multi_user_mode', value: 'false' }, - { label: 'users_can_delete_workspaces', value: 'false' }, - { label: 'limit_user_messages', value: 'false' }, - { label: 'message_limit', value: '25' }, + { label: "multi_user_mode", value: "false" }, + { label: "users_can_delete_workspaces", value: "false" }, + { label: "limit_user_messages", value: "false" }, + { label: "message_limit", value: "25" }, + { label: "logo_filename", value: "anything-llm.png" }, ]; for (let setting of settings) { @@ -24,7 +25,7 @@ async function main() { } main() - .catch(e => { + .catch((e) => { console.error(e); process.exit(1); })