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