commit 03b3066c7babb846d02ffcca2f4649b3a85cb814 Author: timothycarambat Date: Tue Jul 25 13:56:36 2023 -0700 VectorAdmin v0.0.1-beta diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..ede2a57 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +backend/storage/vector-cache/** +**/*.db +document-processor/hotdir/** +document-processor/v-env/** +**/node_modules/ +**/dist/ +**/v-env/ +**/__pycache__/ +**/.env +**/.env.* +!frontend/.env.production +!**/*.placeholderdb \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..94f480d --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5d91ac6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +v-env +.env +!.env.example + +node_modules +__pycache__ +v-env +.DS_Store +yarn.lock diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..59f4a2f --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v18.13.0 \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..f16fa1f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "cSpell.words": [ + "Inngest" + ] +} \ No newline at end of file diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..7c95064 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,8 @@ +SERVER_PORT=3001 + +# temp Root account login +SYS_EMAIL=root@vectoradmin.com +SYS_PASSWORD=password + +JWT_SECRET="YoUr_RaNd0M_sTr1nG" +# STORAGE_DIR= diff --git a/backend/.gitignore b/backend/.gitignore new file mode 100644 index 0000000..bbfb994 --- /dev/null +++ b/backend/.gitignore @@ -0,0 +1,8 @@ +.env.production +.env.development +logs/server.log +*.db +!*.placeholderdb +*.sqbpro +node_modules +storage/vector-cache/*.json \ No newline at end of file diff --git a/backend/.nvmrc b/backend/.nvmrc new file mode 100644 index 0000000..59f4a2f --- /dev/null +++ b/backend/.nvmrc @@ -0,0 +1 @@ +v18.13.0 \ No newline at end of file diff --git a/backend/endpoints/auth.js b/backend/endpoints/auth.js new file mode 100644 index 0000000..f4737c7 --- /dev/null +++ b/backend/endpoints/auth.js @@ -0,0 +1,235 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); +const { SystemSettings } = require("../models/systemSettings"); +const { User } = require("../models/user"); +const { reqBody, makeJWT } = require("../utils/http"); +const bcrypt = require("bcrypt"); + +function authenticationEndpoints(app) { + if (!app) return; + + app.post("/auth/login", async (request, response) => { + try { + const { email, password } = reqBody(request); + if (!email || !password) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[002] No email or password provided.", + }); + return; + } + + if (email === process.env.SYS_EMAIL) { + const completeSetup = (await User.count('role = "admin"')) > 0; + if (completeSetup) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[004] Invalid login credentials.", + }); + return; + } + } + + const existingUser = await User.get(`email = '${email}'`); + if (!existingUser) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[001] Invalid login credentials.", + }); + return; + } + + if (!bcrypt.compareSync(password, existingUser.password)) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[002] Invalid login credentials.", + }); + return; + } + + response.status(200).json({ + valid: true, + user: existingUser, + token: makeJWT( + { id: existingUser.id, email: existingUser.email }, + "30d" + ), + message: null, + }); + return; + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/auth/create-account", async (request, response) => { + try { + const { email, password } = reqBody(request); + if (!email || !password) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[002] No email or password provided.", + }); + return; + } + + const adminCount = await User.count('role = "admin"'); + if (adminCount === 0) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: + "[000] System setup has not been completed - account creation disabled.", + }); + return; + } + + const existingUser = await User.get(`email = '${email}'`); + if (!!existingUser) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[001] Account already exists by this email - use another.", + }); + return; + } + + const allowingAccounts = await SystemSettings.get( + 'label = "allow_account_creation"' + ); + if ( + !!allowingAccounts && + allowingAccounts.value !== null && + allowingAccounts.value === "false" + ) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[003] Access denied.", + }); + return; + } + + const domainRestriction = await SystemSettings.get( + 'label = "account_creation_domain_scope"' + ); + if ( + !!domainRestriction && + domainRestriction.value !== null && + !email.includes(domainRestriction.value) + ) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[003] Invalid account creation values.", + }); + return; + } + + const { user, message } = await User.create({ email, password }); + if (!user) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message, + }); + return; + } + + await User.addToAllOrgs(user.id); + response.status(200).json({ + user, + valid: true, + token: makeJWT({ id: user.id, email: user.email }, "30d"), + message: null, + }); + return; + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/auth/transfer-root", async (request, response) => { + try { + const { email, password } = reqBody(request); + if (!email || !password) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[002] No email or password provided.", + }); + return; + } + + const adminCount = await User.count('role = "admin"'); + if (adminCount > 0) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: + "[000] System setup has already been completed - you cannot do this again.", + }); + return; + } + + const existingUser = await User.get(`email = '${email}'`); + if (!!existingUser) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message: "[001] Account already exists by this email - use another.", + }); + return; + } + + const { user, message } = await User.create({ + email, + password, + role: "admin", + }); + if (!user) { + response.status(200).json({ + user: null, + valid: false, + token: null, + message, + }); + return; + } + + response.status(200).json({ + user, + valid: true, + token: makeJWT({ id: user.id, email: user.email }, "30d"), + message: null, + }); + return; + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); +} + +module.exports = { authenticationEndpoints }; diff --git a/backend/endpoints/system.js b/backend/endpoints/system.js new file mode 100644 index 0000000..a4113c5 --- /dev/null +++ b/backend/endpoints/system.js @@ -0,0 +1,87 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); +const { SystemSettings } = require("../models/systemSettings"); +const { systemInit } = require("../utils/boot"); +const { reqBody, userFromSession } = require("../utils/http"); +// const { validateTablePragmas } = require("../utils/database"); + +function systemEndpoints(app) { + if (!app) return; + + app.get("/ping", (_, response) => { + response.sendStatus(200); + }); + + app.get("/migrate", async (_, response) => { + // await validateTablePragmas(true); + response.sendStatus(200).end(); + }); + + app.get("/system/setting/:label/exists", async (request, response) => { + try { + const { label } = request.params; + if (!SystemSettings.supportedFields.includes(label)) { + response.status(404).json({ label, exists: false }); + return; + } + + const config = await SystemSettings.get(`label = '${label}'`); + response.status(200).json({ label, exists: !!config?.value }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/system/setting/:label", async (request, response) => { + try { + const { label } = request.params; + if (!SystemSettings.supportedFields.includes(label)) { + response.status(404).json({ label, value: null }); + return; + } + + const config = await SystemSettings.get(`label = '${label}'`); + if (SystemSettings.privateField.includes(label)) { + response.status(200).json({ + ...config, + value: new Array((config?.value?.length || 0) + 1).join("*"), + }); + } else { + response.status(200).json(config); + } + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/system/update-settings", async (request, response) => { + try { + const { config } = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + const { success, error } = await SystemSettings.updateSettings(config); + response.status(200).json({ success, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/boot", async (_, response) => { + try { + await systemInit(); + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); +} + +module.exports = { systemEndpoints }; diff --git a/backend/endpoints/v1/document-processor/index.js b/backend/endpoints/v1/document-processor/index.js new file mode 100644 index 0000000..fd0d1cb --- /dev/null +++ b/backend/endpoints/v1/document-processor/index.js @@ -0,0 +1,51 @@ +const { DocumentProcessor } = require("../../../models/documentProcessor"); +const { userFromSession } = require("../../../utils/http"); + +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + +function documentProcessorEndpoints(app) { + if (!app) return; + + app.get("/v1/document-processor/status", async function (request, response) { + try { + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + const online = await DocumentProcessor.status(); + response.sendStatus(online ? 200 : 503).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get( + "/v1/document-processor/filetypes", + async function (request, response) { + try { + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const types = await DocumentProcessor.acceptedFileTypes(); + if (!types) { + response.sendStatus(404).end(); + return; + } + + response.status(200).json({ types }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { documentProcessorEndpoints }; diff --git a/backend/endpoints/v1/documents/index.js b/backend/endpoints/v1/documents/index.js new file mode 100644 index 0000000..9db8b8b --- /dev/null +++ b/backend/endpoints/v1/documents/index.js @@ -0,0 +1,275 @@ +const { DocumentVectors } = require("../../../models/documentVectors"); +const { Organization } = require("../../../models/organization"); +const { + OrganizationConnection, +} = require("../../../models/organizationConnection"); +const { + OrganizationWorkspace, +} = require("../../../models/organizationWorkspace"); +const { WorkspaceDocument } = require("../../../models/workspaceDocument"); +const { userFromSession, reqBody } = require("../../../utils/http"); +const { + updateEmbeddingJob, +} = require("../../../utils/jobs/updateEmbeddingJob"); +const { + createDeleteEmbeddingJob, +} = require("../../../utils/jobs/createDeleteEmbeddingJob"); +const { readJSON } = require("../../../utils/storage"); +const { validEmbedding } = require("../../../utils/tokenizer"); +const { documentDeletedJob } = require("../../../utils/jobs/documentDeleteJob"); +const { cloneDocumentJob } = require("../../../utils/jobs/cloneDocumentJob"); + +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + +function documentEndpoints(app) { + if (!app) return; + + app.get("/v1/document/:id", async function (request, response) { + try { + const { id } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const document = await WorkspaceDocument.get(`id = ${id}`); + response.status(200).json({ document }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.delete("/v1/document/:id", async function (request, response) { + try { + const { id } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const document = await WorkspaceDocument.get(`id = ${id}`); + const organization = await Organization.get( + `id = ${document.organization_id}` + ); + const workspace = await OrganizationWorkspace.get( + `id = ${document.workspace_id}` + ); + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + await documentDeletedJob( + organization, + workspace, + document, + connector, + user + ); + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/document/:id/fragments", async function (request, response) { + try { + const { id } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const fragments = await DocumentVectors.where(`document_id = ${id}`); + response.status(200).json({ fragments }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/document/:id/fragment", async function (request, response) { + try { + const { id } = request.params; + const { newText } = reqBody(request); + const { length, valid } = validEmbedding(newText); + + if (length === 0 || !valid) { + response.status(412).json({ + success: false, + error: "Invalid new text to embed for fragment.", + }); + return; + } + + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const fragment = await DocumentVectors.get(`id = ${id}`); + if (!fragment) { + response.sendStatus(404).end(); + return; + } + + const document = await WorkspaceDocument.get( + `id = ${fragment.document_id}` + ); + const workspace = await OrganizationWorkspace.get( + `id = ${document.workspace_id}` + ); + const organization = await Organization.get( + `id = ${document.organization_id}` + ); + const connector = await OrganizationConnection.get( + `organization_id = ${document.organization_id}` + ); + await updateEmbeddingJob( + fragment, + document, + organization, + workspace, + connector, + user, + newText + ); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.delete("/v1/document/:id/fragment", async function (request, response) { + try { + const { id } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const fragment = await DocumentVectors.get(`id = ${id}`); + if (!fragment) { + response.sendStatus(404).end(); + return; + } + + const document = await WorkspaceDocument.get( + `id = ${fragment.document_id}` + ); + const workspace = await OrganizationWorkspace.get( + `id = ${document.workspace_id}` + ); + const organization = await Organization.get( + `id = ${document.organization_id}` + ); + const connector = await OrganizationConnection.get( + `organization_id = ${document.organization_id}` + ); + await createDeleteEmbeddingJob( + fragment, + workspace, + organization, + connector, + user + ); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/document/:id/source", async function (request, response) { + try { + const { id } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const document = await WorkspaceDocument.get(`id = ${id}`); + const filepath = WorkspaceDocument.vectorFilepath(document); + console.log(filepath); + const source = await readJSON(filepath).then((res) => { + const data = {}; + Object.values(res).map((d) => { + data[d.vectorDbId] = { ...d }; + }); + return data; + }); + response.status(200).json({ ...source }); + } catch (e) { + console.log(e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/document/:id/clone", async function (request, response) { + try { + const { id } = request.params; + const { toWorkspaceId } = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const document = await WorkspaceDocument.get(`id = ${id}`); + if (!document) { + response + .status(404) + .json({ success: false, error: "Document does not exist" }); + return; + } + + const workspace = await OrganizationWorkspace.get( + `id = ${toWorkspaceId}` + ); + if (!workspace) { + response.status(404).json({ + success: false, + error: "Destination workspace does not exist", + }); + return; + } + + const organization = await Organization.get( + `id = ${workspace.organization_id}` + ); + const connector = await OrganizationConnection.get( + `organization_id = ${workspace.organization_id}` + ); + if (!connector) { + response.status(404).json({ + success: false, + error: "Organization connector does not exist", + }); + return; + } + + await cloneDocumentJob( + organization, + workspace, + document, + connector, + user + ); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e); + response.sendStatus(500).end(); + } + }); +} + +module.exports = { documentEndpoints }; diff --git a/backend/endpoints/v1/index.js b/backend/endpoints/v1/index.js new file mode 100644 index 0000000..841fd11 --- /dev/null +++ b/backend/endpoints/v1/index.js @@ -0,0 +1,35 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); +const { userFromSession } = require("../../utils/http"); +const { documentProcessorEndpoints } = require("./document-processor"); +const { documentEndpoints } = require("./documents"); +const { organizationEndpoints } = require("./organizations"); +const { userEndpoints } = require("./users"); +const { workspaceEndpoints } = require("./workspaces"); + +function v1Endpoints(app) { + if (!app) return; + app.get("/v1/valid-session-token", async function (request, response) { + try { + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + userEndpoints(app); + organizationEndpoints(app); + workspaceEndpoints(app); + documentEndpoints(app); + documentProcessorEndpoints(app); +} + +module.exports = { v1Endpoints }; diff --git a/backend/endpoints/v1/organizations/index.js b/backend/endpoints/v1/organizations/index.js new file mode 100644 index 0000000..9019b33 --- /dev/null +++ b/backend/endpoints/v1/organizations/index.js @@ -0,0 +1,509 @@ +const { Organization } = require("../../../models/organization"); +const { OrganizationApiKey } = require("../../../models/organizationApiKey"); +const { + OrganizationConnection, +} = require("../../../models/organizationConnection"); +const { + OrganizationWorkspace, +} = require("../../../models/organizationWorkspace"); +const { Queue } = require("../../../models/queue"); +const { User } = require("../../../models/user"); +const { WorkspaceDocument } = require("../../../models/workspaceDocument"); +const { reqBody, userFromSession } = require("../../../utils/http"); +const { createSyncJob } = require("../../../utils/jobs/createSyncJob"); +const { selectConnector } = require("../../../utils/vectordatabases/providers"); +const { + validateNewDatabaseConnector, +} = require("../../../utils/vectordatabases/validateNewDatabaseConnector"); +const { + validateUpdatedDatabaseConnector, +} = require("../../../utils/vectordatabases/validateUpdatedDatabaseConnector"); + +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + +function organizationEndpoints(app) { + if (!app) return; + + app.post("/v1/org/create", async function (request, response) { + try { + const { orgName } = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const { organization, message } = await Organization.create( + orgName, + user.id + ); + if (!organization) { + response.status(200).json({ + organization: null, + error: message ?? "Failed to create organization.", + }); + return; + } + + response.status(200).json({ organization, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/orgs", async function (request, response) { + try { + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + const organizations = await Organization.whereWithOwner( + user.id, + null, + null, + "ORDER BY createdAt ASC" + ); + response.status(200).json({ organizations, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/orgs/all", async function (request, response) { + try { + const user = await userFromSession(request); + if (!user || user.role !== "admin") { + response.sendStatus(403).end(); + return; + } + const organizations = await Organization.where(`id IS NOT NULL`); + response.status(200).json({ organizations, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/org/:slug", async function (request, response) { + try { + const { slug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + response.status(200).json({ organization, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/org/:slug/api-key", async function (request, response) { + try { + const { slug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + const apiKey = await OrganizationApiKey.get( + `organization_id = ${organization.id}` + ); + if (!apiKey) { + response.status(200).json({ + organization: null, + error: "No api key for that organization.", + }); + return; + } + + response.status(200).json({ apiKey, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/org/:slug/connection", async function (request, response) { + try { + const { slug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + if (!connector) { + response.status(200).json({ + connector: null, + error: "No data connector for that organization.", + }); + return; + } + + response.status(200).json({ connector, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/org/:slug/add-connection", async function (request, response) { + try { + const { slug } = request.params; + const { config } = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + if (!!connector) { + response.status(200).json({ + connector: null, + error: "Vector database connector already exists for organization.", + }); + return; + } + + const result = await validateNewDatabaseConnector(organization, config); + response.status(200).json(result); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post( + "/v1/org/:slug/update-connection", + async function (request, response) { + try { + const { slug } = request.params; + const { config } = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + if (!connector) { + response.status(200).json({ + connector: null, + error: "No Vector database connector exists for organization.", + }); + return; + } + + const result = await validateUpdatedDatabaseConnector( + connector, + config + ); + response.status(200).json(result); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/org/:slug/connector/:command", + async function (request, response) { + try { + const { slug, command } = request.params; + const body = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + if (!connector) { + response.status(200).json({ + connector: null, + error: "No Vector database connector exists for organization.", + }); + return; + } + + const VectorDb = selectConnector(connector); + const { result, error } = await VectorDb[command](body); + response.status(200).json({ result, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get( + "/v1/org/:slug/connector/:connectorId/sync", + async function (request, response) { + try { + const { slug, connectorId } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const connector = await OrganizationConnection.get( + `id = ${connectorId}` + ); + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization || !connector) { + response.status(200).json({ + organization: null, + error: "No org or connector for org found.", + }); + return; + } + + const { job, error } = await createSyncJob( + organization, + connector, + user + ); + response.status(200).json({ job, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get("/v1/org/:slug/workspaces", async function (request, response) { + try { + const { slug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org found." }); + return; + } + + const workspaces = await OrganizationWorkspace.forOrganization( + organization.id + ); + response.status(200).json({ workspaces }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/org/:slug/jobs", async function (request, response) { + try { + const { slug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org found." }); + return; + } + + const jobs = await Queue.where( + `organizationId = ${organization.id}`, + null, + "ORDER BY createdAt DESC" + ); + for (const job of jobs) { + const { id, email, role } = await User.get(`id = ${job.runByUserId}`); + job.runByUser = { id, email, role }; + } + response.status(200).json({ jobs }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get("/v1/org/:slug/documents", async function (request, response) { + try { + const { slug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org found." }); + return; + } + + const documents = await WorkspaceDocument.where( + `organization_id = ${organization.id}`, + null, + true + ); + response.status(200).json({ documents }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.get( + "/v1/org/:slug/statistics/:statistic", + async function (request, response) { + try { + const { slug, statistic } = request.params; + const user = await userFromSession(request); + if (!user) { + response.status(200).json({ value: null }); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org found." }); + return; + } + + const methods = { + documents: "countForEntity", + vectors: "calcVectors", + "cache-size": "calcVectorCache", + }; + + if (!Object.keys(methods).includes(statistic)) { + response + .status(200) + .json({ value: null, error: "Invalid statistic." }); + return; + } + + const value = await WorkspaceDocument[methods[statistic]]( + "organization_id", + organization.id + ); + response.status(200).json({ value }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { organizationEndpoints }; diff --git a/backend/endpoints/v1/users/index.js b/backend/endpoints/v1/users/index.js new file mode 100644 index 0000000..4963083 --- /dev/null +++ b/backend/endpoints/v1/users/index.js @@ -0,0 +1,93 @@ +const { User } = require("../../../models/user"); +const { userFromSession, reqBody } = require("../../../utils/http"); + +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + +function userEndpoints(app) { + if (!app) return; + + app.get("/v1/users", async function (request, response) { + try { + const user = await userFromSession(request); + if (!user || user.role !== "admin") { + response.sendStatus(403).end(); + return; + } + + const users = await User.whereWithOrgs(`role != 'root'`); + response.status(200).json({ users }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.delete("/v1/users/:userId", async function (request, response) { + try { + const { userId } = request.params; + const user = await userFromSession(request); + if (!user || user.role !== "admin") { + response.sendStatus(403).end(); + return; + } + + if (user.id == userId) { + response + .status(200) + .json({ success: false, error: "You cannot delete yourself." }); + return; + } + + await User.delete(`id = ${userId}`); + response.status(200).json({ success: true, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/user/new", async function (request, response) { + try { + const { email, password, role = "default" } = reqBody(request); + const user = await userFromSession(request); + if (!user || user.role !== "admin") { + response.sendStatus(403).end(); + return; + } + + const { user: newUser, message } = await User.create({ + email, + password, + role, + }); + await User.addToAllOrgs(newUser.id); + response.status(200).json({ success: !!newUser, error: message }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); + + app.post("/v1/users/:userId", async function (request, response) { + try { + const { userId } = request.params; + const updates = reqBody(request); + + const user = await userFromSession(request); + if (!user || user.role !== "admin") { + response.sendStatus(403).end(); + return; + } + + const { success, error } = await User.update(userId, updates); + response.status(200).json({ success, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + }); +} + +module.exports = { userEndpoints }; diff --git a/backend/endpoints/v1/workspaces/index.js b/backend/endpoints/v1/workspaces/index.js new file mode 100644 index 0000000..c8ceaf5 --- /dev/null +++ b/backend/endpoints/v1/workspaces/index.js @@ -0,0 +1,385 @@ +const { Organization } = require("../../../models/organization"); +const { + OrganizationConnection, +} = require("../../../models/organizationConnection"); +const { + OrganizationWorkspace, +} = require("../../../models/organizationWorkspace"); +const { WorkspaceDocument } = require("../../../models/workspaceDocument"); +const { userFromSession, reqBody } = require("../../../utils/http"); +const { setupMulter } = require("../../..//utils/files/multer"); +const { DocumentProcessor } = require("../../../models/documentProcessor"); +const { addDocumentJob } = require("../../../utils/jobs/addDocumentsJob"); +const { + workspaceDeletedJob, +} = require("../../../utils/jobs/workspaceDeletedJob"); +const { newWorkspaceJob } = require("../../../utils/jobs/newWorkspaceJob"); +const { + createWorkspaceSyncJob, +} = require("../../../utils/jobs/createWorkspaceSyncJob"); +const { cloneWorkspaceJob } = require("../../../utils/jobs/cloneWorkspaceJob"); + +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + +function workspaceEndpoints(app) { + if (!app) return; + const { handleUploads } = setupMulter(); + + app.post( + "/v1/org/:orgSlug/new-workspace", + async function (request, response) { + try { + const { orgSlug } = request.params; + const { workspaceName } = reqBody(request); + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${orgSlug}'` + ); + if (!organization) { + response + .status(200) + .json({ workspace: null, error: "No org by that slug." }); + return; + } + + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + if (!connector) { + response.status(200).json({ + workspace: null, + error: + "You need to connect to a vector database before doing this.", + }); + return; + } + + const { workspace, message: error } = + await OrganizationWorkspace.safeCreate( + workspaceName, + organization.id, + connector + ); + await newWorkspaceJob(organization, workspace, connector, user); + response.status(200).json({ workspace, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get( + "/v1/org/:orgSlug/workspace/:wsSlug", + async function (request, response) { + try { + const { orgSlug, wsSlug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${orgSlug}'` + ); + if (!organization) { + response + .status(200) + .json({ organization: null, error: "No org by that slug." }); + return; + } + + const workspace = await OrganizationWorkspace.get(`slug = '${wsSlug}'`); + response.status(200).json({ workspace, error: null }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.delete( + "/v1/org/:orgSlug/workspace/:wsSlug", + async function (request, response) { + try { + const { orgSlug, wsSlug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${orgSlug}'` + ); + if (!organization) { + response + .status(200) + .json({ workspace: null, error: "No org by that slug." }); + return; + } + + const workspace = await OrganizationWorkspace.get(`slug = '${wsSlug}'`); + if (!workspace) { + response + .status(200) + .json({ workspace: null, error: "No workspace by that slug." }); + return; + } + + const documents = await WorkspaceDocument.where( + `workspace_id = ${workspace.id}` + ); + await OrganizationWorkspace.delete(`id = ${workspace.id}`); + + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + await workspaceDeletedJob( + organization, + workspace, + connector, + documents, + user + ); + response.sendStatus(200).end(); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get( + "/v1/org/:orgSlug/workspace/:wsSlug/documents", + async function (request, response) { + try { + const { orgSlug, wsSlug } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${orgSlug}'` + ); + const workspace = await OrganizationWorkspace.get( + `slug = '${wsSlug}' AND organization_id = ${organization.id}` + ); + if (!organization || !workspace) { + response + .status(200) + .json({ organization: null, error: "No org found." }); + return; + } + + const documents = await WorkspaceDocument.where( + `organization_id = ${organization.id} AND workspace_id = ${workspace.id}`, + null, + true + ); + response.status(200).json({ documents }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.get( + "/v1/org/:slug/workspace/:workspaceSlug/statistics/:statistic", + async function (request, response) { + try { + const { slug, workspaceSlug, statistic } = request.params; + const user = await userFromSession(request); + if (!user) { + response.status(200).json({ value: null }); + return; + } + + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + const workspace = await OrganizationWorkspace.bySlugAndOrg( + workspaceSlug, + organization.id + ); + if (!organization || !workspace) { + response + .status(200) + .json({ organization: null, error: "No org or workspace found." }); + return; + } + + const methods = { + documents: "countForEntity", + vectors: "calcVectors", + "cache-size": "calcVectorCache", + }; + + if (!Object.keys(methods).includes(statistic)) { + response + .status(200) + .json({ value: null, error: "Invalid statistic." }); + return; + } + + console.log(workspace); + const value = await WorkspaceDocument[methods[statistic]]( + "workspace_id", + workspace.id + ); + response.status(200).json({ value }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); + + app.post( + "/v1/org/:slug/workspace/:workspaceSlug/upload", + handleUploads.single("file"), + async function (request, response) { + const { slug, workspaceSlug } = request.params; + const { originalname } = request.file; + const processingOnline = await DocumentProcessor.status(); + + if (!processingOnline) { + response.status(500).json({ + success: false, + error: `Python processing API is not online. Document ${originalname} will not be processed automatically.`, + }); + return; + } + + const { + success, + reason, + metadata = [], + } = await DocumentProcessor.prepareForEmbed(originalname); + if (!success) { + response.status(500).json({ success: false, error: reason }); + return false; + } + + try { + const user = await userFromSession(request); + const organization = await Organization.get(`slug = '${slug}'`); + const workspace = await OrganizationWorkspace.get( + `slug = '${workspaceSlug}' AND organization_id = ${organization.id}` + ); + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + await addDocumentJob( + metadata, + organization, + workspace, + connector, + user + ); + } catch (e) { + console.error(e.message); + response.status(500).json({ success: false, error: e.message }); + return; + } + + response.status(200).json({ success: true, error: null }); + } + ); + + app.post( + "/v1/org/:slug/workspace/:workspaceSlug/clone", + handleUploads.single("file"), + async function (request, response) { + try { + const { slug, workspaceSlug } = request.params; + const { newWorkspaceName } = reqBody(request); + + const user = await userFromSession(request); + const organization = await Organization.get(`slug = '${slug}'`); + const workspace = await OrganizationWorkspace.get( + `slug = '${workspaceSlug}' AND organization_id = ${organization.id}` + ); + const connector = await OrganizationConnection.get( + `organization_id = ${organization.id}` + ); + + await cloneWorkspaceJob( + organization, + workspace, + connector, + newWorkspaceName, + user + ); + } catch (e) { + console.error(e.message); + response.status(500).json({ success: false, error: e.message }); + return; + } + + response.status(200).json({ success: true, error: null }); + } + ); + + app.get( + "/v1/org/:slug/connector/:connectorId/sync/:workspaceSlug", + async function (request, response) { + try { + const { slug, workspaceSlug, connectorId } = request.params; + const user = await userFromSession(request); + if (!user) { + response.sendStatus(403).end(); + return; + } + + const connector = await OrganizationConnection.get( + `id = ${connectorId}` + ); + const organization = await Organization.getWithOwner( + user.id, + `slug = '${slug}'` + ); + const workspace = await OrganizationWorkspace.bySlugAndOrg( + workspaceSlug, + organization.id + ); + if (!organization || !connector || !workspace) { + response.status(200).json({ + organization: null, + error: "No org or connector for org found.", + }); + return; + } + + const { job, error } = await createWorkspaceSyncJob( + organization, + workspace, + connector, + user + ); + response.status(200).json({ job, error }); + } catch (e) { + console.log(e.message, e); + response.sendStatus(500).end(); + } + } + ); +} + +module.exports = { workspaceEndpoints }; diff --git a/backend/index.js b/backend/index.js new file mode 100644 index 0000000..63b0aef --- /dev/null +++ b/backend/index.js @@ -0,0 +1,64 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); + +const express = require("express"); +const bodyParser = require("body-parser"); +const cors = require("cors"); +const path = require("path"); +const { validatedRequest } = require("./utils/middleware/validatedRequest"); +const { validSessionForUser } = require("./utils/http"); +const { systemEndpoints } = require("./endpoints/system"); +const { systemInit } = require("./utils/boot"); +const { authenticationEndpoints } = require("./endpoints/auth"); +const { v1Endpoints } = require("./endpoints/v1"); +// const { validateTablePragmas } = require("./utils/database"); +const app = express(); +const apiRouter = express.Router(); + +app.use(cors({ origin: true })); +app.use(bodyParser.text()); +app.use(bodyParser.json()); +app.use( + bodyParser.urlencoded({ + extended: true, + }) +); + +authenticationEndpoints(app); +apiRouter.use("/system/*", validatedRequest); +systemEndpoints(app); + +apiRouter.use("/v1/*", validSessionForUser); +v1Endpoints(app); + +if (process.env.NODE_ENV !== "development") { + app.use( + express.static(path.resolve(__dirname, "public"), { extensions: ["js"] }) + ); + + app.use("/", function (_, response) { + response.sendFile(path.join(__dirname, "public", "index.html")); + }); +} + +app.all("*", function (_, response) { + response.sendStatus(404); +}); + +app + .listen(process.env.SERVER_PORT || 3001, async () => { + // await validateTablePragmas(); + await systemInit(); + console.log( + `Example app listening on port ${process.env.SERVER_PORT || 3001}` + ); + }) + .on("error", function (err) { + process.once("SIGUSR2", function () { + process.kill(process.pid, "SIGUSR2"); + }); + process.on("SIGINT", function () { + process.kill(process.pid, "SIGINT"); + }); + }); diff --git a/backend/models/documentProcessor.js b/backend/models/documentProcessor.js new file mode 100644 index 0000000..172b14d --- /dev/null +++ b/backend/models/documentProcessor.js @@ -0,0 +1,42 @@ +// When running locally will occupy the 0.0.0.0 hostname space but when deployed inside +// of docker this endpoint is not exposed so it is only on the Docker instances internal network +// so no additional security is needed on the endpoint directly. Auth is done however by the express +// middleware prior to leaving the node-side of the application so that is good enough >:) +const PYTHON_API = "http://0.0.0.0:8888"; +const DocumentProcessor = { + status: async function () { + return await fetch(`${PYTHON_API}`) + .then((res) => res.ok) + .catch((e) => false); + }, + acceptedFileTypes: async function () { + return await fetch(`${PYTHON_API}/accepts`) + .then((res) => { + if (!res.ok) throw new Error("Could not reach"); + return res.json(); + }) + .then((res) => res) + .catch(() => null); + }, + prepareForEmbed: async function (filename = null) { + if (!filename) return false; + return await fetch(`${PYTHON_API}/process`, { + method: "POST", + headers: { + "Content-Type": "application/json", + }, + body: JSON.stringify({ filename }), + }) + .then((res) => { + if (!res.ok) throw new Error("Response could not be completed"); + return res.json(); + }) + .then((res) => res) + .catch((e) => { + console.log(e.message); + return { success: false, reason: e.message }; + }); + }, +}; + +module.exports.DocumentProcessor = DocumentProcessor; diff --git a/backend/models/documentVectors.js b/backend/models/documentVectors.js new file mode 100644 index 0000000..ed21c78 --- /dev/null +++ b/backend/models/documentVectors.js @@ -0,0 +1,106 @@ +// const { checkForMigrations } = require("../utils/database"); + +const DocumentVectors = { + tablename: "document_vectors", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + docId TEXT NOT NULL, + vectorId TEXT NOT NULL, + document_id INTEGER NOT NULL, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')), + + FOREIGN KEY (document_id) REFERENCES workspace_documents (id) ON DELETE CASCADE + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for ${this.tablename} migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return [ + // { + // colName: "id", + // execCmd: `CREATE TRIGGER IF NOT EXISTS Trg_LastUpdated AFTER UPDATE ON ${this.tablename} + // FOR EACH ROW + // BEGIN + // UPDATE ${this.tablename} SET lastUpdatedAt = (strftime('%s', 'now')) WHERE id = old.id; + // END`, + // doif: true, + // }, + ]; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + + createMany: async function (vectors = []) { + if (vectors.length === 0) return; + const db = await this.db(); + const stmt = await db.prepare( + `INSERT INTO ${this.tablename} (docId, vectorId, document_id) VALUES (?,?,?)` + ); + + for (const vector of vectors) { + stmt.run([vector.docId, vector.vectorId, vector.documentId]); + } + + stmt.finalize(); + db.close(); + return; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + where: async function (clause = null, limit = null, orderBy = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + } ${orderBy ? orderBy : ""}` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, + delete: async function (id = null) { + const db = await this.db(); + await db.get(`DELETE FROM ${this.tablename} WHERE id = ${id}`); + db.close(); + return; + }, +}; + +module.exports.DocumentVectors = DocumentVectors; diff --git a/backend/models/organization.js b/backend/models/organization.js new file mode 100644 index 0000000..4418bb2 --- /dev/null +++ b/backend/models/organization.js @@ -0,0 +1,152 @@ +// const { checkForMigrations } = require("../utils/database"); +const uuidAPIKey = require("uuid-apikey"); +const slugify = require("slugify"); +const { OrganizationUser } = require("./organizationUser"); +const { OrganizationApiKey } = require("./organizationApiKey"); + +const Organization = { + tablename: "organizations", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + uuid TEXT NOT NULL UNIQUE, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')) + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + makeKey: () => { + return `org-${uuidAPIKey.create().apiKey}`; + }, + create: async function (orgName = "", adminId) { + if (!orgName) + return { organization: null, message: "No Organization name provided." }; + var slug = slugify(orgName, { lower: true }); + + const existingBySlug = await this.get(`slug = '${slug}'`); + if (!!existingBySlug) { + const slugSeed = Math.floor(10000000 + Math.random() * 90000000); + slug = slugify(`${orgName}-${slugSeed}`, { lower: true }); + } + + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (name, slug, uuid) VALUES (?, ?, ?)`, + [orgName, slug, this.makeKey()] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE ORGANIZATION.", message); + return { organization: null, message }; + } + + const organization = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id}` + ); + db.close(); + + await OrganizationUser.create(adminId, organization.id); + await OrganizationApiKey.create(organization.id); + return { organization, message: null }; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + getWithOwner: async function (userId, clause = "") { + const db = await this.db(); + const result = await db + .get( + `SELECT * FROM ${this.tablename} as org + LEFT JOIN organization_users as org_users + ON org_users.organization_id = org.id + WHERE org_users.user_id = ${userId} AND ${clause}` + ) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return { ...result, id: result.organization_id }; + }, + where: async function (clause = null, limit = null, orderBy = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + } ${!!orderBy ? orderBy : ""}` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, + whereWithOwner: async function ( + userId, + clause = null, + limit = null, + orderBy = null + ) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} as org + LEFT JOIN organization_users as org_users + ON org_users.organization_id = org.id + WHERE org_users.user_id = ${userId} ${clause ? `AND ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + } ${!!orderBy ? orderBy : ""}` + ); + db.close(); + + return results; + }, +}; + +module.exports.Organization = Organization; diff --git a/backend/models/organizationApiKey.js b/backend/models/organizationApiKey.js new file mode 100644 index 0000000..0f008a7 --- /dev/null +++ b/backend/models/organizationApiKey.js @@ -0,0 +1,100 @@ +// const { checkForMigrations } = require("../utils/database"); +const uuidAPIKey = require("uuid-apikey"); + +const OrganizationApiKey = { + tablename: "organization_api_keys", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + organization_id INTEGER NOT NULL, + apiKey TEXT NOT NULL UNIQUE, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')), + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + makeKey: () => { + return `vdms-${uuidAPIKey.create().apiKey}`; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + create: async function (organizationId = 0) { + const db = await this.db(); + const { success, message } = await db + .run( + `INSERT INTO ${this.tablename} (organization_id, apiKey) VALUES (?, ?)`, + [organizationId, this.makeKey()] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error( + "FAILED TO CREATE ORGANIZATION_API_KEYS RELATIONSHIP.", + message + ); + return false; + } + return true; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + where: async function (clause = null, limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, +}; + +module.exports.OrganizationApiKey = OrganizationApiKey; diff --git a/backend/models/organizationConnection.js b/backend/models/organizationConnection.js new file mode 100644 index 0000000..2bea560 --- /dev/null +++ b/backend/models/organizationConnection.js @@ -0,0 +1,141 @@ +// const { checkForMigrations } = require("../utils/database"); + +const OrganizationConnection = { + supportedConnectors: ["chroma", "pinecone"], + tablename: "organization_connections", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + type TEXT NOT NULL, + settings TEXT NOT NULL, + organization_id INTEGER NOT NULL, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')), + + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + `, + writable: ["type", "settings"], + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + create: async function ( + organizationId = 0, + connectionType = "", + connectionSettings = {} + ) { + if (!this.supportedConnectors.includes(connectionType)) + throw new Error(`Unsupport connector ${connectionType} provided.`); + + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (organization_id, type, settings) VALUES (?, ?, ?)`, + [organizationId, connectionType, JSON.stringify(connectionSettings)] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + db.close(); + if (!success) { + console.error( + "FAILED TO CREATE ORGANIZATION_CONNECTION RELATIONSHIP.", + message + ); + return false; + } + + const connector = await this.get(`id = ${id}`); + return connector; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + update: async function (id = null, data = {}) { + if (!id) throw new Error("No workspace id provided for update"); + + const validKeys = Object.keys(data).filter((key) => + this.writable.includes(key) + ); + const values = Object.values(data); + if (validKeys.length === 0 || validKeys.length !== values.length) + return { connector: { id }, message: "No valid fields to update!" }; + + const template = `UPDATE ${this.tablename} SET ${validKeys.map((key) => { + return `${key}=?`; + })} WHERE id = ?`; + const db = await this.db(); + const { success, message } = await db + .run(template, [...values, id]) + .then(() => { + return { success: true, message: null }; + }) + .catch((error) => { + return { success: false, message: error.message }; + }); + + db.close(); + if (!success) { + console.error(message); + return null; + } + + const updatedConnector = await this.get(`id = ${id}`); + return updatedConnector; + }, + where: async function (clause = null, limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, +}; + +module.exports.OrganizationConnection = OrganizationConnection; diff --git a/backend/models/organizationUser.js b/backend/models/organizationUser.js new file mode 100644 index 0000000..c937dce --- /dev/null +++ b/backend/models/organizationUser.js @@ -0,0 +1,125 @@ +// const { checkForMigrations } = require("../utils/database"); + +const OrganizationUser = { + tablename: "organization_users", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + organization_id INTEGER NOT NULL, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')), + + FOREIGN KEY (user_id) REFERENCES users (id) ON DELETE CASCADE, + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + createMany: async function (userId, organizationIds = []) { + if (organizationIds.length === 0) return; + const db = await this.db(); + const stmt = await db.prepare( + `INSERT INTO ${this.tablename} (user_id, organization_id) VALUES (?,?)` + ); + + for (const orgId of organizationIds) { + stmt.run([userId, orgId]); + } + + stmt.finalize(); + db.close(); + return; + }, + create: async function (userId = 0, organizationId = 0) { + const db = await this.db(); + const { success, message } = await db + .run( + `INSERT INTO ${this.tablename} (user_id, organization_id) VALUES (?, ?)`, + [userId, organizationId] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error( + "FAILED TO CREATE ORGANIZATION_USER RELATIONSHIP.", + message + ); + return false; + } + return true; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + where: async function (clause = null, limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, + delete: async function (clause = null) { + const db = await this.db(); + await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`); + return; + }, + updateOrgPermissions: async function (userId, _orgIds = []) { + const orgIds = _orgIds.filter((id) => id !== null).map((id) => Number(id)); + if (orgIds.length === 0) return; // Must belong to at least one org. + + await this.delete(`user_id = ${userId}`); + await this.createMany(userId, orgIds); + }, +}; + +module.exports.OrganizationUser = OrganizationUser; diff --git a/backend/models/organizationWorkspace.js b/backend/models/organizationWorkspace.js new file mode 100644 index 0000000..21f5ab2 --- /dev/null +++ b/backend/models/organizationWorkspace.js @@ -0,0 +1,199 @@ +// const { checkForMigrations } = require("../utils/database"); +const uuidAPIKey = require("uuid-apikey"); +const slugify = require("slugify"); +const { WorkspaceDocument } = require("./workspaceDocument"); +const { selectConnector } = require("../utils/vectordatabases/providers"); + +const OrganizationWorkspace = { + tablename: "organization_workspaces", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + slug TEXT NOT NULL UNIQUE, + uuid TEXT NOT NULL UNIQUE, + organization_id INTEGER NOT NULL, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')), + + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + makeKey: () => { + return `ws-${uuidAPIKey.create().apiKey}`; + }, + // Will check the relevant connector to make sure the slug and namespace/collection map + safeCreate: async function ( + workspaceName = "", + organizationId = 0, + dbConnectorRecord + ) { + if (!workspaceName) + return { workspace: null, message: "No Workspace name provided." }; + const connector = selectConnector(dbConnectorRecord); + + var slug = slugify(workspaceName, { lower: true }); + const existingDbBySlug = await this.get(`slug = '${slug}'`); + const existingConnectorNamespace = await connector.namespace(slug); + + // If there was a name collision in the DB or the vectorstore collection - make a unique slug. + // as the namespace/collection will always be the slug. + if (!!existingDbBySlug || !!existingConnectorNamespace) { + const slugSeed = Math.floor(10000000 + Math.random() * 90000000); + slug = slugify(`${workspaceName}-${slugSeed}`, { lower: true }); + } + + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (name, slug, uuid, organization_id) VALUES (?, ?, ?, ?)`, + [workspaceName, slug, this.makeKey(), organizationId] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE WORKSPACE.", message); + return { workspace: null, message }; + } + + const workspace = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id}` + ); + db.close(); + + return { workspace, message: null }; + }, + create: async function (workspaceName = "", organizationId = 0) { + if (!workspaceName) + return { workspace: null, message: "No Workspace name provided." }; + var slug = slugify(workspaceName, { lower: true }); + + const existingBySlug = await this.get(`slug = '${slug}'`); + if (!!existingBySlug) { + const slugSeed = Math.floor(10000000 + Math.random() * 90000000); + slug = slugify(`${workspaceName}-${slugSeed}`, { lower: true }); + } + + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (name, slug, uuid, organization_id) VALUES (?, ?, ?, ?)`, + [workspaceName, slug, this.makeKey(), organizationId] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE WORKSPACE.", message); + return { workspace: null, message }; + } + + const workspace = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id}` + ); + db.close(); + + return { workspace, message: null }; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + forOrganization: async function (organizationId) { + const orgWorkspaces = await this.where( + `organization_id = ${organizationId}` + ); + const workspaces = []; + for (const workspace of orgWorkspaces) { + workspaces.push({ + ...workspace, + documentCount: await WorkspaceDocument.count( + `workspace_id = ${workspace.id}` + ), + }); + } + + return workspaces; + }, + bySlugAndOrg: async function (wsSlug, organizationId = null) { + return await this.get( + `slug = '${wsSlug}' AND organization_id = ${organizationId}` + ); + }, + where: async function (clause = null, limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, + deleteAllForOrganization: async function (organizationId = "") { + const db = await this.db(); + await db.get( + `DELETE FROM ${this.tablename} WHERE organization_id = ${organizationId}` + ); + return; + }, + delete: async function (clause = null) { + const db = await this.db(); + await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`); + return; + }, +}; + +module.exports.OrganizationWorkspace = OrganizationWorkspace; diff --git a/backend/models/queue.js b/backend/models/queue.js new file mode 100644 index 0000000..8f54878 --- /dev/null +++ b/backend/models/queue.js @@ -0,0 +1,139 @@ +// const { checkForMigrations } = require("../utils/database"); +require("dotenv").config(); + +const Queue = { + tablename: "jobs", + status: { + pending: "pending", + failed: "failed", + complete: "complete", + }, + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + taskName TEXT NOT NULL, + status TEXT NOT NULL DEFAULT 'pending', + data TEXT NOT NULL, + result TEXT NOT NULL, + runByUserId INTEGER DEFAULT NULL, + organizationId INTEGER, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')) + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/job_queue.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + create: async function (task, data = {}, userId = null, organizationId) { + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (taskName, data, result, runByUserId, organizationId) VALUES (?, ?, ?, ?, ?)`, + [task, JSON.stringify(data), JSON.stringify({}), userId, organizationId] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE JOB.", message); + return { job: null, error: message }; + } + + const job = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id}` + ); + db.close(); + + return { job, error: null }; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + where: async function (clause = null, limit = null, orderBy = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + } ${orderBy ? orderBy : ""}` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, + sendJob: async function (data) { + await fetch(`http://127.0.0.1:3355/send`, { + method: "POST", + body: JSON.stringify(data), + }) + .then((res) => res.ok) + .catch((e) => { + console.error("Failed to send background worker job", e.message); + }); + }, + updateJob: async function (jobId, status, result) { + const template = `UPDATE ${this.tablename} SET status=?, result=? WHERE id = ?`; + const db = await this.db(); + const { success, message } = await db + .run(template, [status, JSON.stringify(result), jobId]) + .then(() => { + return { success: true, message: null }; + }) + .catch((error) => { + return { success: false, message: error.message }; + }); + + db.close(); + if (!success) { + console.error(message); + return null; + } + + const updatedJob = await this.get(`id = ${jobId}`); + return updatedJob; + }, +}; + +module.exports.Queue = Queue; diff --git a/backend/models/systemSettings.js b/backend/models/systemSettings.js new file mode 100644 index 0000000..b90612c --- /dev/null +++ b/backend/models/systemSettings.js @@ -0,0 +1,116 @@ +// const { checkForMigrations } = require("../utils/database"); + +const SystemSettings = { + supportedFields: [ + "allow_account_creation", + "account_creation_domain_scope", + "open_ai_api_key", + ], + privateField: ["open_ai_api_key"], + tablename: "system_settings", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + label TEXT UNIQUE NOT NULL, + value TEXT, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')) + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + where: async function (clause = null, limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + + return results; + }, + updateSettings: async function (updates = {}) { + const validConfigKeys = Object.keys(updates).filter((key) => + this.supportedFields.includes(key) + ); + for (const key of validConfigKeys) { + const existingRecord = await this.get(`label = '${key}'`); + if (!existingRecord) { + const db = await this.db(); + const value = updates[key] === null ? null : String(updates[key]); + const { success, message } = await db + .run(`INSERT INTO ${this.tablename} (label, value) VALUES (?, ?)`, [ + key, + value, + ]) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + db.close(); + if (!success) { + console.error("FAILED TO ADD SYSTEM CONFIG OPTION", message); + return { success: false, error: message }; + } + } else { + const db = await this.db(); + const value = updates[key] === null ? null : String(updates[key]); + const { success, message } = await db + .run(`UPDATE ${this.tablename} SET label=?,value=? WHERE id = ?`, [ + key, + value, + existingRecord.id, + ]) + .then(() => { + return { success: true, message: null }; + }) + .catch((error) => { + return { success: false, message: error.message }; + }); + + db.close(); + if (!success) { + console.error("FAILED TO UPDATE SYSTEM CONFIG OPTION", message); + return { success: false, error: message }; + } + } + } + return { success: true, error: null }; + }, +}; + +module.exports.SystemSettings = SystemSettings; diff --git a/backend/models/user.js b/backend/models/user.js new file mode 100644 index 0000000..3636db3 --- /dev/null +++ b/backend/models/user.js @@ -0,0 +1,201 @@ +// const { checkForMigrations } = require("../utils/database"); + +const User = { + tablename: "users", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + email TEXT UNIQUE, + password TEXT NOT NULL, + role TEXT NOT NULL DEFAULT "default", + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')) + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON; CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit})` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + create: async function ({ email, password, role = null }) { + const bcrypt = require("bcrypt"); + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (email, password, role) VALUES(?, ?, ?)`, + [email, bcrypt.hashSync(password, 10), role ?? "default"] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE USER.", message); + return { user: null, message }; + } + + const user = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id} ` + ); + db.close(); + + return { user, message: null }; + }, + get: async function (clause = "") { + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause} `) + .then((res) => res || null); + if (!result) return null; + db.close(); + + return result; + }, + where: async function (clause = null, limit = null) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + } ` + ); + db.close(); + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + } ` + ); + db.close(); + + return count; + }, + whereWithOrgs: async function (clause = null, limit = null) { + const { Organization } = require("./organization"); + const { OrganizationUser } = require("./organizationUser"); + + const users = await this.where(clause, limit); + const organizations = await Organization.where(); + + for (const user of users) { + const memberships = await OrganizationUser.where(`user_id = ${user.id}`); + delete user.password; + user.memberships = []; + + for (const membership of memberships) { + const org = organizations.find( + (org) => org.id === membership.organization_id + ); + user.memberships.push({ ...org, organization_id: org.id }); + } + } + + return users; + }, + addToAllOrgs: async function (userId = null) { + if (!userId) return false; + const { Organization } = require("./organization"); + const { OrganizationUser } = require("./organizationUser"); + + const organizations = await Organization.where(); + if (!organizations.length) return; + + const orgIds = organizations.map((org) => org.id); + await OrganizationUser.createMany(userId, orgIds); + return; + }, + delete: async function (clause = null) { + const db = await this.db(); + await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`); + return; + }, + update: async function (userId, updates = {}) { + const user = await this.get(`id = ${userId}`); + if (!user) return { success: false, error: "User does not exist." }; + const { email, password, role, memberships } = updates; + const toUpdate = {}; + + if (user.email !== email && email?.length > 0) { + const usedEmail = !!(await this.get(`email = '${email}'`)); + if (usedEmail) + return { success: false, error: `${email} is already in use.` }; + toUpdate.email = email; + } + + if (!!password) { + const bcrypt = require("bcrypt"); + toUpdate.password = bcrypt.hashSync(password, 10); + } + + if (user.role !== role && ["admin", "default"].includes(role)) { + // If was existing admin and that has been changed + // make sure at least one admin exists + if (user.role === "admin") { + const validAdminCount = (await this.count(`role = 'admin'`)) > 1; + if (!validAdminCount) + return { + success: false, + error: `There would be no admins if this action was completed. There must be at least one admin.`, + }; + } + + toUpdate.role = role; + } + + if (Object.keys(toUpdate).length !== 0) { + const values = Object.values(toUpdate); + const template = `UPDATE ${this.tablename} SET ${Object.keys( + toUpdate + ).map((key) => { + return `${key}=?`; + })} WHERE id = ?`; + + const db = await this.db(); + const { success, message } = await db + .run(template, [...values, userId]) + .then(() => { + return { success: true, message: null }; + }) + .catch((error) => { + return { success: false, message: error.message }; + }); + + db.close(); + if (!success) { + console.error(message); + return { success: false, error: message }; + } + } + + const { OrganizationUser } = require("./organizationUser"); + await OrganizationUser.updateOrgPermissions(user.id, memberships); + return { success: true, error: null }; + }, +}; + +module.exports.User = User; diff --git a/backend/models/workspaceDocument.js b/backend/models/workspaceDocument.js new file mode 100644 index 0000000..1804fad --- /dev/null +++ b/backend/models/workspaceDocument.js @@ -0,0 +1,208 @@ +// const { checkForMigrations } = require("../utils/database"); +const path = require("path"); +const { v5 } = require("uuid"); +const { fetchMetadata } = require("../utils/storage"); +const { DocumentVectors } = require("./documentVectors"); + +const WorkspaceDocument = { + tablename: "workspace_documents", + colsInit: ` + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + docId TEXT NOT NULL UNIQUE, + organization_id INTEGER NOT NULL, + workspace_id INTEGER NOT NULL, + createdAt TEXT DEFAULT (strftime('%s', 'now')), + lastUpdatedAt TEXT DEFAULT (strftime('%s', 'now')), + + FOREIGN KEY (organization_id) REFERENCES organizations (id) ON DELETE CASCADE + FOREIGN KEY (workspace_id) REFERENCES organization_workspaces (id) ON DELETE CASCADE + `, + // migrateTable: async function () { + // console.log(`\x1b[34m[MIGRATING]\x1b[0m Checking for Document migrations`); + // const db = await this.db(false); + // await checkForMigrations(this, db); + // }, + migrations: function () { + return []; + }, + db: async function (tracing = true) { + const sqlite3 = require("sqlite3").verbose(); + const { open } = require("sqlite"); + const path = require("path"); + const dbFilePath = path.resolve(__dirname, "../storage/vdbms.db"); + const db = await open({ + filename: dbFilePath, + driver: sqlite3.Database, + }); + + await db.exec( + `PRAGMA foreign_keys = ON;CREATE TABLE IF NOT EXISTS ${this.tablename} (${this.colsInit});` + ); + + if (tracing) db.on("trace", (sql) => console.log(sql)); + return db; + }, + vectorFilenameRaw: function (documentName, workspaceId) { + const document = { name: documentName, workspace_id: workspaceId }; + return this.vectorFilename(document); + }, + vectorFilename: function (document) { + if (!document?.name) return null; + return v5(`ws_${document.workspace_id}_` + document.name, v5.URL); + }, + vectorFilepath: function (document) { + const cacheFilename = this.vectorFilename(document); + return path.resolve( + __dirname, + `../storage/vector-cache/${cacheFilename}.json` + ); + }, + create: async function (data = null) { + if (!data) return; + const db = await this.db(); + const { id, success, message } = await db + .run( + `INSERT INTO ${this.tablename} (docId, name, workspace_id, organization_id) VALUES (?,?,?,?)`, + [data.id, data.name, data.workspaceId, data.organizationId] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + console.log(error); + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + db.close(); + console.error("FAILED TO CREATE DOCUMENT.", message); + return { document: null, message }; + } + + const document = await db.get( + `SELECT * FROM ${this.tablename} WHERE id = ${id}` + ); + db.close(); + + return { document, message: null }; + }, + + // Used by workers paginateAndStore method to bulk create documents easily during import. + // document items in array must have documentId, name, metadata (this is document specific - not each vector chunk.), workspaceId, organizationId, + createMany: async function (documents = []) { + if (documents.length === 0) return; + const db = await this.db(); + const stmt = await db.prepare( + `INSERT INTO ${this.tablename} (docId, name, workspace_id, organization_id) VALUES (?,?,?,?)` + ); + + for (const document of documents) { + stmt.run([ + document.documentId, + document.name, + document.workspaceId, + document.organizationId, + ]); + } + + stmt.finalize(); + db.close(); + return; + }, + get: async function (clause = "", withReferences = false) { + const { OrganizationWorkspace } = require("./organizationWorkspace"); + const db = await this.db(); + const result = await db + .get(`SELECT * FROM ${this.tablename} WHERE ${clause}`) + .then((res) => res || null); + if (!result) return null; + db.close(); + if (!withReferences) return result; + + return { + ...result, + workspace: await OrganizationWorkspace.get(`id = ${result.workspace_id}`), + }; + }, + where: async function (clause = null, limit = null, withReferences = false) { + const db = await this.db(); + const results = await db.all( + `SELECT * FROM ${this.tablename} ${clause ? `WHERE ${clause}` : ""} ${ + !!limit ? `LIMIT ${limit}` : "" + }` + ); + db.close(); + if (!withReferences) return results; + + const { OrganizationWorkspace } = require("./organizationWorkspace"); + for (const res of results) { + res.workspace = await OrganizationWorkspace.get( + `id = ${res.workspace_id}` + ); + } + + return results; + }, + count: async function (clause = null) { + const db = await this.db(); + const { count } = await db.get( + `SELECT COUNT(*) as count FROM ${this.tablename} ${ + clause ? `WHERE ${clause}` : "" + }` + ); + db.close(); + + return count; + }, + countForEntity: async function (field = "organization_id", value = null) { + return await this.count(`${field} = ${value}`); + }, + calcVectors: async function (field = "organization_id", value = null) { + const documents = await this.where(`${field} = ${value}`); + + var vectorCount = 0; + for (const document of documents) { + try { + const _count = await DocumentVectors.count( + `document_id = ${document.id}` + ); + vectorCount += _count || 0; + } catch (e) { + console.error(e); + } + } + + return vectorCount; + }, + calcVectorCache: async function (field = "organization_id", value = null) { + const documents = await this.where(`${field} = ${value}`); + + var totalBytes = 0; + for (const document of documents) { + try { + const cacheFilepath = this.vectorFilepath(document); + const metadata = await fetchMetadata(cacheFilepath); + totalBytes += Number(metadata?.size); + } catch (e) { + console.error(e); + } + } + + return totalBytes; + }, + deleteWhere: async function (clause = null) { + const db = await this.db(); + await db.get(`DELETE FROM ${this.tablename} WHERE ${clause}`); + db.close(); + return; + }, + delete: async function (id = null) { + const db = await this.db(); + await db.get(`DELETE FROM ${this.tablename} WHERE id = ${id}`); + db.close(); + return; + }, +}; + +module.exports.WorkspaceDocument = WorkspaceDocument; diff --git a/backend/package.json b/backend/package.json new file mode 100644 index 0000000..4d82a02 --- /dev/null +++ b/backend/package.json @@ -0,0 +1,42 @@ +{ + "name": "vector-data-management-server", + "version": "0.0.1-beta", + "description": "Server endpoints to process vector database connections", + "main": "index.js", + "author": "Timothy Carambat (Mintplex Labs)", + "license": "MIT", + "private": false, + "engines": { + "node": ">=18.12.1" + }, + "scripts": { + "dev": "NODE_ENV=development nodemon --trace-warnings index.js", + "start": "NODE_ENV=production node index.js", + "lint": "yarn prettier --write ./endpoints ./models ./utils index.js" + }, + "dependencies": { + "@pinecone-database/pinecone": "^0.1.6", + "bcrypt": "^5.1.0", + "body-parser": "^1.20.2", + "chromadb": "^1.5.3", + "cors": "^2.8.5", + "dotenv": "^16.0.3", + "express": "^4.18.2", + "js-tiktoken": "^1.0.7", + "jsonwebtoken": "^8.5.1", + "langchain": "^0.0.107", + "moment": "^2.29.4", + "multer": "^1.4.5-lts.1", + "openai": "^3.3.0", + "pinecone-client": "^1.1.0", + "slugify": "^1.6.6", + "sqlite": "^4.2.1", + "sqlite3": "^5.1.6", + "uuid": "^9.0.0", + "uuid-apikey": "^1.5.3" + }, + "devDependencies": { + "nodemon": "^2.0.22", + "prettier": "^2.4.1" + } +} diff --git a/backend/storage/job_queue.placeholderdb b/backend/storage/job_queue.placeholderdb new file mode 100644 index 0000000..e69de29 diff --git a/backend/storage/vdbms.placeholderdb b/backend/storage/vdbms.placeholderdb new file mode 100644 index 0000000..e69de29 diff --git a/backend/utils/boot/index.js b/backend/utils/boot/index.js new file mode 100644 index 0000000..c990985 --- /dev/null +++ b/backend/utils/boot/index.js @@ -0,0 +1,88 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); +const { OrganizationApiKey } = require("../../models/organizationApiKey"); +const { + OrganizationConnection, +} = require("../../models/organizationConnection"); +const { OrganizationUser } = require("../../models/organizationUser"); +const { SystemSettings } = require("../../models/systemSettings"); +const { User } = require("../../models/user"); + +function findOrCreateDBFile() { + const fs = require("fs"); + const path = require("path"); + const storageFolder = path.resolve(__dirname, `../../storage/`); + const dbPath = `${storageFolder}vdbms.db`; + if (!fs.existsSync(storageFolder)) fs.mkdirSync(storageFolder); + if (fs.existsSync(dbPath)) return; + fs.writeFileSync(dbPath, ""); + console.log("SQLite db created on boot."); + return; +} + +function setupVectorCacheStorage() { + const fs = require("fs"); + const path = require("path"); + const storageFolder = path.resolve(__dirname, `../../storage/vector-cache`); + if (!fs.existsSync(storageFolder)) + fs.mkdirSync(storageFolder, { recursive: true }); + console.log("Storage folder for vector-cache created."); + return; +} + +// Init all tables so to not try to reference foreign key +// tables that may not exist +async function initTables() { + (await SystemSettings.db()).close(); + (await User.db()).close(); + (await OrganizationUser.db()).close(); + (await OrganizationApiKey.db()).close(); + (await OrganizationUser.db()).close(); + (await OrganizationConnection.db()).close(); +} + +async function systemInit() { + try { + await findOrCreateDBFile(); + await setupVectorCacheStorage(); + await initTables(); + const completeSetup = (await User.count('role = "admin"')) > 0; + if (completeSetup) return; + + process.env.SYS_EMAIL = process.env.SYS_EMAIL ?? "root@vectoradmin.com"; + process.env.SYS_PASSWORD = process.env.SYS_PASSWORD ?? "password"; + + const existingRootUser = await User.get( + `email = '${process.env.SYS_EMAIL}' AND role = 'root'` + ); + if (existingRootUser) return; + + const bcrypt = require("bcrypt"); + const userDb = await User.db(); + const { success, message } = await userDb + .run( + `INSERT INTO ${User.tablename} (email, password, role) VALUES (?, ?, 'root')`, + [process.env.SYS_EMAIL, bcrypt.hashSync(process.env.SYS_PASSWORD, 10)] + ) + .then((res) => { + return { id: res.lastID, success: true, message: null }; + }) + .catch((error) => { + return { id: null, success: false, message: error.message }; + }); + + if (!success) { + userDb.close(); + console.error("FAILED TO CREATE ROOT USER.", message); + } + + console.log("Root user created with credentials"); + return; + } catch (e) { + console.error("systemInit", e.message, e); + return; + } +} + +module.exports.systemInit = systemInit; diff --git a/backend/utils/files/multer.js b/backend/utils/files/multer.js new file mode 100644 index 0000000..9f117b8 --- /dev/null +++ b/backend/utils/files/multer.js @@ -0,0 +1,26 @@ +function setupMulter() { + const multer = require("multer"); + + // Handle File uploads for auto-uploading. + const storage = multer.diskStorage({ + destination: function (_, _, cb) { + const path = require("path"); + const uploadOutput = path.resolve( + __dirname, + `../../../document-processor/hotdir` + ); + cb(null, uploadOutput); + }, + filename: function (_, file, cb) { + cb(null, file.originalname); + }, + }); + const upload = multer({ + storage, + }); + return { handleUploads: upload }; +} + +module.exports = { + setupMulter, +}; diff --git a/backend/utils/http/index.js b/backend/utils/http/index.js new file mode 100644 index 0000000..e719a63 --- /dev/null +++ b/backend/utils/http/index.js @@ -0,0 +1,82 @@ +process.env.NODE_ENV === "development" + ? require("dotenv").config({ path: `.env.${process.env.NODE_ENV}` }) + : require("dotenv").config(); +const JWT = require("jsonwebtoken"); +const { User } = require("../../models/user"); +const SECRET = process.env.JWT_SECRET; + +function reqBody(request) { + return typeof request.body === "string" + ? JSON.parse(request.body) + : request.body; +} + +function queryParams(request) { + return request.query; +} + +function makeJWT(info = {}, expiry = "30d") { + if (!SECRET) throw new Error("Cannot create JWT as JWT_SECRET is unset."); + return JWT.sign(info, SECRET, { expiresIn: expiry }); +} + +function validSessionForUser(request, response, next) { + const auth = request.header("Authorization"); + const email = request.header("requester-email"); + const token = auth ? auth.split(" ")[1] : null; + + // If no token present or no email + if (!token || !email) { + response.sendStatus(403).end(); + return; + } + + // If the decode of the JWT fails totally. + const valid = decodeJWT(token); + if (!valid) { + response.sendStatus(403).end(); + return; + } + + // If the decoded JWT [email] prop does not match the requester header + if (valid.email.toLowerCase() !== email.toLowerCase()) { + response.sendStatus(403).end(); + return; + } + + next(); +} + +async function userFromSession(request) { + const auth = request.header("Authorization"); + const email = request.header("requester-email"); + const token = auth ? auth.split(" ")[1] : null; + + if (!token || !email) { + return null; + } + + const valid = decodeJWT(token); + if (!valid) { + return null; + } + + const user = await User.get(`id = ${valid.id}`); + return user; +} + +function decodeJWT(jwtToken) { + try { + return JWT.verify(jwtToken, SECRET); + } catch {} + return null; +} + +module.exports = { + reqBody, + userFromSession, + validSessionForUser, + queryParams, + makeJWT, + decodeJWT, +}; diff --git a/backend/utils/jobs/addDocumentsJob/index.js b/backend/utils/jobs/addDocumentsJob/index.js new file mode 100644 index 0000000..e391ce3 --- /dev/null +++ b/backend/utils/jobs/addDocumentsJob/index.js @@ -0,0 +1,31 @@ +const { Queue } = require("../../../models/queue"); + +async function addDocumentJob( + metadata, + organization, + workspace, + connector, + user +) { + const taskName = `${connector.type}/addDocument`; + const jobData = { documents: metadata, organization, workspace, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + addDocumentJob, +}; diff --git a/backend/utils/jobs/cloneDocumentJob/index.js b/backend/utils/jobs/cloneDocumentJob/index.js new file mode 100644 index 0000000..b57b6f0 --- /dev/null +++ b/backend/utils/jobs/cloneDocumentJob/index.js @@ -0,0 +1,31 @@ +const { Queue } = require("../../../models/queue"); + +async function cloneDocumentJob( + organization, + workspace, + document, + connector, + user +) { + const taskName = `${connector.type}/cloneDocument`; + const jobData = { organization, workspace, document, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + cloneDocumentJob, +}; diff --git a/backend/utils/jobs/cloneWorkspaceJob/index.js b/backend/utils/jobs/cloneWorkspaceJob/index.js new file mode 100644 index 0000000..911f305 --- /dev/null +++ b/backend/utils/jobs/cloneWorkspaceJob/index.js @@ -0,0 +1,31 @@ +const { Queue } = require("../../../models/queue"); + +async function cloneWorkspaceJob( + organization, + workspace, + connector, + newWorkspaceName, + user +) { + const taskName = `${connector.type}/cloneWorkspace`; + const jobData = { organization, workspace, newWorkspaceName, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + cloneWorkspaceJob, +}; diff --git a/backend/utils/jobs/createDeleteEmbeddingJob/index.js b/backend/utils/jobs/createDeleteEmbeddingJob/index.js new file mode 100644 index 0000000..6b71e96 --- /dev/null +++ b/backend/utils/jobs/createDeleteEmbeddingJob/index.js @@ -0,0 +1,31 @@ +const { Queue } = require("../../../models/queue"); + +async function createDeleteEmbeddingJob( + documentVector, + workspace, + organization, + connector, + user +) { + const taskName = `${connector.type}/deleteFragment`; + const jobData = { documentVector, workspace, organization, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + createDeleteEmbeddingJob, +}; diff --git a/backend/utils/jobs/createSyncJob/index.js b/backend/utils/jobs/createSyncJob/index.js new file mode 100644 index 0000000..1de3b09 --- /dev/null +++ b/backend/utils/jobs/createSyncJob/index.js @@ -0,0 +1,28 @@ +const { Queue } = require("../../../models/queue"); + +async function createSyncJob(organization, connector, user) { + const taskName = `${connector.type}/sync`; + // const hasPendingJob = await Queue.get(`organizationId = ${organization.id} AND status = '${Queue.status.pending}' AND taskName = '${taskName}'`); + // if (hasPendingJob) return { job: hasPendingJob, error: null }; + + const jobData = { organization, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + createSyncJob, +}; diff --git a/backend/utils/jobs/createWorkspaceSyncJob/index.js b/backend/utils/jobs/createWorkspaceSyncJob/index.js new file mode 100644 index 0000000..c005122 --- /dev/null +++ b/backend/utils/jobs/createWorkspaceSyncJob/index.js @@ -0,0 +1,35 @@ +const { Queue } = require("../../../models/queue"); + +async function createWorkspaceSyncJob( + organization, + workspace, + connector, + user +) { + const taskName = `${connector.type}/sync-workspace`; + const hasPendingJob = await Queue.get( + `organizationId = ${organization.id} AND status = '${Queue.status.pending}' AND taskName = '${taskName}'` + ); + if (hasPendingJob) return { job: hasPendingJob, error: null }; + + const jobData = { organization, workspace, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + createWorkspaceSyncJob, +}; diff --git a/backend/utils/jobs/documentDeleteJob/index.js b/backend/utils/jobs/documentDeleteJob/index.js new file mode 100644 index 0000000..6125950 --- /dev/null +++ b/backend/utils/jobs/documentDeleteJob/index.js @@ -0,0 +1,31 @@ +const { Queue } = require("../../../models/queue"); + +async function documentDeletedJob( + organization, + workspace, + document, + connector, + user +) { + const taskName = `${connector.type}/deleteDocument`; + const jobData = { organization, workspace, connector, document }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + documentDeletedJob, +}; diff --git a/backend/utils/jobs/newWorkspaceJob/index.js b/backend/utils/jobs/newWorkspaceJob/index.js new file mode 100644 index 0000000..20a7a76 --- /dev/null +++ b/backend/utils/jobs/newWorkspaceJob/index.js @@ -0,0 +1,25 @@ +const { Queue } = require("../../../models/queue"); + +async function newWorkspaceJob(organization, workspace, connector, user) { + const taskName = `workspace/new`; + const jobData = { organization, workspace, connector }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + newWorkspaceJob, +}; diff --git a/backend/utils/jobs/updateEmbeddingJob/index.js b/backend/utils/jobs/updateEmbeddingJob/index.js new file mode 100644 index 0000000..01b589f --- /dev/null +++ b/backend/utils/jobs/updateEmbeddingJob/index.js @@ -0,0 +1,40 @@ +const { Queue } = require("../../../models/queue"); + +async function updateEmbeddingJob( + documentVector, + document, + organization, + workspace, + connector, + user, + newText +) { + const taskName = `${connector.type}/updateFragment`; + const jobData = { + documentVector, + document, + organization, + workspace, + connector, + newText, + }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + updateEmbeddingJob, +}; diff --git a/backend/utils/jobs/workspaceDeletedJob/index.js b/backend/utils/jobs/workspaceDeletedJob/index.js new file mode 100644 index 0000000..74ff4a4 --- /dev/null +++ b/backend/utils/jobs/workspaceDeletedJob/index.js @@ -0,0 +1,38 @@ +const { Queue } = require("../../../models/queue"); + +async function workspaceDeletedJob( + organization, + workspace, + connector, + documents, + user +) { + if (!connector) { + console.log( + `No VectorDB connector found - no workspace/delete job will be queued.` + ); + return; + } + + const taskName = `workspace/delete`; + const jobData = { organization, workspace, connector, documents }; + const { job, error } = await Queue.create( + taskName, + jobData, + user.id, + organization.id + ); + if (!!error) return { job, error }; + await Queue.sendJob({ + name: taskName, + data: { + jobId: job.id, + ...jobData, + }, + }); + return { job, error: null }; +} + +module.exports = { + workspaceDeletedJob, +}; diff --git a/backend/utils/middleware/validatedRequest.js b/backend/utils/middleware/validatedRequest.js new file mode 100644 index 0000000..daf88aa --- /dev/null +++ b/backend/utils/middleware/validatedRequest.js @@ -0,0 +1,8 @@ +function validatedRequest(request, response, next) { + // NOOP + next(); +} + +module.exports = { + validatedRequest, +}; diff --git a/backend/utils/openAi/index.js b/backend/utils/openAi/index.js new file mode 100644 index 0000000..9827248 --- /dev/null +++ b/backend/utils/openAi/index.js @@ -0,0 +1,24 @@ +const { Configuration, OpenAIApi } = require("openai"); +class OpenAi { + constructor(apiKey = "") { + const config = new Configuration({ apiKey }); + const openai = new OpenAIApi(config); + this.openai = openai; + } + + async embedTextChunk(textChunk = "") { + const { + data: { data }, + } = await this.openai.createEmbedding({ + model: "text-embedding-ada-002", + input: textChunk, + }); + return data.length > 0 && data[0].hasOwnProperty("embedding") + ? data[0].embedding + : null; + } +} + +module.exports = { + OpenAi, +}; diff --git a/backend/utils/storage/index.js b/backend/utils/storage/index.js new file mode 100644 index 0000000..8b468ad --- /dev/null +++ b/backend/utils/storage/index.js @@ -0,0 +1,86 @@ +const fs = require("fs"); +const path = require("path"); + +async function readJSON(filepath = null) { + if (!fs.existsSync(filepath)) + throw new Error(`${filepath} does not exist in storage`); + try { + const rawData = fs.readFileSync(filepath, "utf-8"); + return JSON.parse(rawData); + } catch (e) { + console.error(e.message); + throw new Error(`Could not read JSON data from ${filepath}`, e.message); + } +} + +async function fetchMetadata(filepath = null) { + if (!fs.existsSync(filepath)) + throw new Error(`${filepath} does not exist in storage`); + try { + return fs.statSync(filepath); + } catch (e) { + console.error(e.message); + throw new Error(`Could not read JSON data from ${filepath}`, e.message); + } +} + +async function deleteVectorCacheFile(digestFilename = null) { + try { + const filepath = path.resolve( + __dirname, + `../../storage/vector-cache/${digestFilename}.json` + ); + if (!fs.existsSync(filepath)) return false; + + console.log(`Removing vector-cache file ${digestFilename}`); + fs.rmSync(filepath); + return true; + } catch (e) { + console.error(`deleteVectorCacheFile`, e.message); + return false; + } +} + +// Searches the vector-cache folder for existing information so we dont have to re-embed a +// document and can instead push directly to vector db. +async function cachedVectorInformation( + digestFilename = null, + checkOnly = false +) { + if (!digestFilename) return checkOnly ? false : { exists: false, chunks: [] }; + const file = path.resolve( + __dirname, + `../../storage/vector-cache/${digestFilename}.json` + ); + const exists = fs.existsSync(file); + + if (checkOnly) return exists; + if (!exists) return { exists, chunks: [] }; + + console.log( + `Cached vectorized results of ${digestFilename} found! Using cached data to save on embed costs.` + ); + const rawData = fs.readFileSync(file, "utf8"); + return { exists: true, chunks: JSON.parse(rawData) }; +} + +// vectorData: pre-chunked vectorized data for a given file that includes the proper metadata and chunk-size limit so it can be iterated and dumped into Pinecone, etc +// digestFilename is the document title with `ws_` all digested prepended to the title and hashed with v5.URL so each cache can be different between workspaces. +async function storeVectorResult(vectorData = [], digestFilename = null) { + if (!digestFilename) return; + + const folder = path.resolve(__dirname, `../../storage/vector-cache`); + if (!fs.existsSync(folder)) fs.mkdirSync(folder, { recursive: true }); + + const writeTo = path.resolve(folder, `${digestFilename}.json`); + fs.writeFileSync(writeTo, JSON.stringify(vectorData), "utf8"); + return; +} + +module.exports = { + readJSON, + fetchMetadata, + cachedVectorInformation, + storeVectorResult, + deleteVectorCacheFile, +}; diff --git a/backend/utils/tokenizer/index.js b/backend/utils/tokenizer/index.js new file mode 100644 index 0000000..5bd6fb4 --- /dev/null +++ b/backend/utils/tokenizer/index.js @@ -0,0 +1,21 @@ +const { getEncoding } = require("js-tiktoken"); +const MAX_TOKENS = { + cl100k_base: 8192, // https://platform.openai.com/docs/guides/embeddings/second-generation-models +}; + +function countLLMTokens(input) { + const encoding = getEncoding("cl100k_base"); + const tokens = encoding.encode(input); + return { tokens, length: tokens.length }; +} + +// Check if an input is valid length for embedding - we add a buffer just for edge-case incongruencies in the JS port of TikToken. +function validEmbedding(input = "", buffer = 50, model = "cl100k_base") { + const { tokens, length } = countLLMTokens(input); + return { tokens, length, valid: length + buffer <= MAX_TOKENS[model] }; +} + +module.exports = { + MAX_TOKENS, + validEmbedding, +}; diff --git a/backend/utils/vectordatabases/providers/chroma/index.js b/backend/utils/vectordatabases/providers/chroma/index.js new file mode 100644 index 0000000..38a2598 --- /dev/null +++ b/backend/utils/vectordatabases/providers/chroma/index.js @@ -0,0 +1,209 @@ +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { OpenAi } = require("../../../openAi"); +const { v4 } = require("uuid"); +const { DocumentVectors } = require("../../../../models/documentVectors"); +const { toChunks } = require("../../utils"); +const { storeVectorResult } = require("../../../storage"); +const { WorkspaceDocument } = require("../../../../models/workspaceDocument"); + +class Chroma { + constructor(connector) { + this.name = "chroma"; + this.config = this.setConfig(connector); + } + + setConfig(config) { + var { type, settings } = config; + if (typeof settings === "string") settings = JSON.parse(settings); + return { type, settings }; + } + + async connect() { + const { ChromaClient } = require("chromadb"); + const { type, settings } = this.config; + + if (type !== "chroma") + throw new Error("Chroma::Invalid Not a Chroma connector instance."); + + const client = new ChromaClient({ + path: settings.instanceURL, + }); + + const isAlive = await client.heartbeat(); + if (!isAlive) + throw new Error( + "ChromaDB::Invalid Heartbeat received - is the instance online?" + ); + return { client }; + } + + async heartbeat() { + const { client } = await this.connect(); + return { result: await client.heartbeat(), error: null }; + } + + async totalIndicies() { + const { client } = await this.connect(); + const collections = await client.listCollections(); + var totalVectors = 0; + for (const collectionObj of collections) { + const collection = await client + .getCollection({ name: collectionObj.name }) + .catch(() => null); + if (!collection) continue; + totalVectors += await collection.count(); + } + return { result: totalVectors, error: null }; + } + + // Collections === namespaces for Chroma to normalize interfaces + async collections() { + return await this.namespaces(); + } + async namespaces() { + const { client } = await this.connect(); + const allCollections = await client.listCollections(); + const collections = []; + + for (const collectionInfo of allCollections) { + const collection = await client + .getCollection(collectionInfo) + .catch(() => null); + collections.push({ + ...collectionInfo, + count: collection ? await collection.count() : 0, + }); + } + + return collections; + } + + async namespace(name = null) { + if (!name) throw new Error("No namespace value provided."); + const { client } = await this.connect(); + const collection = await client.getCollection({ name }).catch(() => null); + if (!collection) return null; + + return { + ...collection, + count: await collection.count(), + }; + } + + async rawGet(collectionId, pageSize = 10, offset = 0) { + return await fetch( + `${this.config.settings.instanceURL}/api/v1/collections/${collectionId}/get`, + { + method: "POST", + headers: { + accept: "application/json", + "Content-Type": "application/json", + }, + body: JSON.stringify({ + limit: pageSize, + offset: offset, + include: ["embeddings", "documents", "metadatas"], + }), + } + ) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e.message); + return { ids: [], embeddings: [], metadatas: [], documents: [] }; + }); + } + + // Split, embed, and save a given document data that we get from the document processor + // API. + async processDocument( + collectionName, + documentData, + embedderApiKey, + dbDocument + ) { + try { + const openai = new OpenAi(embedderApiKey); + const { pageContent, id, ...metadata } = documentData; + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: 1000, + chunkOverlap: 20, + }); + const textChunks = await textSplitter.splitText(pageContent); + + console.log("Chunks created from document:", textChunks.length); + const documentVectors = []; + const cacheInfo = []; + const vectors = []; + + const submission = { + ids: [], + embeddings: [], + metadatas: [], + documents: [], + }; + + for (const textChunk of textChunks) { + const vectorValues = await openai.embedTextChunk(textChunk); + + if (!!vectorValues) { + const vectorRecord = { + id: v4(), + values: vectorValues, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64 + metadata: { ...metadata, text: textChunk }, + }; + + submission.ids.push(vectorRecord.id); + submission.embeddings.push(vectorRecord.values); + submission.metadatas.push(metadata); + submission.documents.push(textChunk); + + vectors.push(vectorRecord); + documentVectors.push({ + docId: id, + vectorId: vectorRecord.id, + documentId: dbDocument.id, + }); + cacheInfo.push({ + vectorDbId: vectorRecord.id, + values: vectorValues, + metadata: vectorRecord.metadata, + }); + } else { + console.error( + "Could not use OpenAI to embed document chunk! This document will not be recorded." + ); + } + } + + const { client } = await this.connect(); + const collection = await client.getCollection({ name: collectionName }); + + if (vectors.length > 0) { + const chunks = []; + for (const chunk of toChunks(vectors, 500)) chunks.push(chunk); + const additionResult = await collection.add(submission); + if (!additionResult) + return { + success: false, + message: "Failed to push data to Chroma instance.", + }; + } + + await DocumentVectors.createMany(documentVectors); + await storeVectorResult( + cacheInfo, + WorkspaceDocument.vectorFilename(dbDocument) + ); + return { success: true, message: null }; + } catch (e) { + console.error("addDocumentToNamespace", e.message); + return { success: false, message: e.message }; + } + } +} + +module.exports.Chroma = Chroma; diff --git a/backend/utils/vectordatabases/providers/index.js b/backend/utils/vectordatabases/providers/index.js new file mode 100644 index 0000000..8296dd1 --- /dev/null +++ b/backend/utils/vectordatabases/providers/index.js @@ -0,0 +1,26 @@ +const { + OrganizationConnection, +} = require("../../../models/organizationConnection"); +const { Chroma } = require("./chroma"); +const { Pinecone } = require("./pinecone"); + +function selectConnector(organizationConnector) { + const { type } = organizationConnector; + + if (!OrganizationConnection.supportedConnectors.includes(type)) + throw new Error("Unsupported connector for vector database."); + if (organizationConnector.type === "chroma") { + return new Chroma(organizationConnector); + } + + if (organizationConnector.type === "pinecone") { + return new Pinecone(organizationConnector); + } + + throw new Error( + "Could not find supported connector for vector database.", + type + ); +} + +module.exports = { selectConnector }; diff --git a/backend/utils/vectordatabases/providers/pinecone/index.js b/backend/utils/vectordatabases/providers/pinecone/index.js new file mode 100644 index 0000000..c5a67ae --- /dev/null +++ b/backend/utils/vectordatabases/providers/pinecone/index.js @@ -0,0 +1,223 @@ +const { RecursiveCharacterTextSplitter } = require("langchain/text_splitter"); +const { OpenAi } = require("../../../openAi"); +const { v4 } = require("uuid"); +const { DocumentVectors } = require("../../../../models/documentVectors"); +const { toChunks } = require("../../utils"); +const { storeVectorResult } = require("../../../storage"); +const { WorkspaceDocument } = require("../../../../models/workspaceDocument"); + +class Pinecone { + constructor(connector) { + this.name = "pinecone"; + this.config = this.setConfig(connector); + } + + setConfig(config) { + var { type, settings } = config; + if (typeof settings === "string") settings = JSON.parse(settings); + return { type, settings }; + } + + async indexDimensions() { + const { pineconeIndex } = await this.connect(); + const description = await pineconeIndex.describeIndexStats1(); + return Number(description?.dimension || 0); + } + + async connect() { + const { PineconeClient } = require("@pinecone-database/pinecone"); + const { type, settings } = this.config; + + if (type !== "pinecone") + throw new Error("Pinecone::Invalid Not a Pinecone connector instance."); + + const client = new PineconeClient(); + await client.init({ + apiKey: settings.apiKey, + environment: settings.environment, + }); + + const pineconeIndex = client.Index(settings.index); + const { status } = await client.describeIndex({ + indexName: settings.index, + }); + if (!status.ready) throw new Error("Pinecode::Index not ready."); + + return { client, pineconeIndex }; + } + + async totalIndicies() { + const { pineconeIndex } = await this.connect(); + const { namespaces } = await pineconeIndex.describeIndexStats1(); + const totalVectors = Object.values(namespaces).reduce( + (a, b) => a + (b?.vectorCount || 0), + 0 + ); + return { result: totalVectors, error: null }; + } + + // Collections === namespaces for Pinecone to normalize interfaces + async collections() { + return await this.namespaces(); + } + + async namespaces() { + const { pineconeIndex } = await this.connect(); + const { namespaces } = await pineconeIndex.describeIndexStats1(); + const collections = Object.entries(namespaces).map(([name, values]) => { + return { + name, + count: values?.vectorCount || 0, + }; + }); + + return collections; + } + + async namespaceExists(index, namespace = null) { + if (!namespace) throw new Error("No namespace value provided."); + const { namespaces } = await index.describeIndexStats1(); + return namespaces.hasOwnProperty(namespace); + } + + async namespace(name = null) { + if (!name) throw new Error("No namespace value provided."); + const { pineconeIndex } = await this.connect(); + const { namespaces } = await pineconeIndex.describeIndexStats1(); + const collection = namespaces.hasOwnProperty(name) + ? namespaces[name] + : null; + if (!collection) return null; + + return { + name, + ...collection, + count: collection?.vectorCount || 0, + }; + } + + async rawGet(pineconeIndex, namespace, offset = 10, filterRunId = "") { + const data = { + ids: [], + embeddings: [], + metadatas: [], + documents: [], + }; + const queryRequest = { + namespace, + topK: offset, + includeValues: true, + includeMetadata: true, + vector: Array.from({ length: 1536 }, () => 0), + filter: { + runId: { $ne: filterRunId }, + }, + }; + + const queryResult = await pineconeIndex.query({ queryRequest }); + if (!queryResult?.matches || queryResult.matches.length === 0) return data; + + queryResult.matches.forEach((match) => { + const { id, values, metadata } = match; + data.ids.push(id); + data.embeddings.push(values); + data.metadatas.push(metadata); + data.documents.push(metadata?.text ?? ""); + }); + + return data; + } + + // Split, embed, and save a given document data that we get from the document processor + // API. + async processDocument( + namespace, + documentData, + embedderApiKey, + dbDocument, + pineconeIndex + ) { + try { + const openai = new OpenAi(embedderApiKey); + const { pageContent, id, ...metadata } = documentData; + const textSplitter = new RecursiveCharacterTextSplitter({ + chunkSize: 1000, + chunkOverlap: 20, + }); + const textChunks = await textSplitter.splitText(pageContent); + + console.log("Chunks created from document:", textChunks.length); + const documentVectors = []; + const cacheInfo = []; + const vectors = []; + + const submission = { + ids: [], + embeddings: [], + metadatas: [], + documents: [], + }; + + for (const textChunk of textChunks) { + const vectorValues = await openai.embedTextChunk(textChunk); + + if (!!vectorValues) { + const vectorRecord = { + id: v4(), + values: vectorValues, + // [DO NOT REMOVE] + // LangChain will be unable to find your text if you embed manually and dont include the `text` key. + // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64 + metadata: { ...metadata, text: textChunk }, + }; + + submission.ids.push(vectorRecord.id); + submission.embeddings.push(vectorRecord.values); + submission.metadatas.push(metadata); + submission.documents.push(textChunk); + + vectors.push(vectorRecord); + documentVectors.push({ + docId: id, + vectorId: vectorRecord.id, + documentId: dbDocument.id, + }); + cacheInfo.push({ + vectorDbId: vectorRecord.id, + values: vectorValues, + metadata: vectorRecord.metadata, + }); + } else { + console.error( + "Could not use OpenAI to embed document chunk! This document will not be recorded." + ); + } + } + + if (vectors.length > 0) { + const chunks = []; + for (const chunk of toChunks(vectors, 500)) { + chunks.push(chunk); + await pineconeIndex.upsert({ + upsertRequest: { + vectors: [...chunk], + namespace, + }, + }); + } + } + + await DocumentVectors.createMany(documentVectors); + await storeVectorResult( + cacheInfo, + WorkspaceDocument.vectorFilename(dbDocument) + ); + return { success: true, message: null }; + } catch (e) { + console.error("addDocumentToNamespace", e.message); + return { success: false, message: e.message }; + } + } +} + +module.exports.Pinecone = Pinecone; diff --git a/backend/utils/vectordatabases/utils/index.js b/backend/utils/vectordatabases/utils/index.js new file mode 100644 index 0000000..b048a5e --- /dev/null +++ b/backend/utils/vectordatabases/utils/index.js @@ -0,0 +1,9 @@ +function toChunks(arr, size) { + return Array.from({ length: Math.ceil(arr.length / size) }, (_v, i) => + arr.slice(i * size, i * size + size) + ); +} + +module.exports = { + toChunks, +}; diff --git a/backend/utils/vectordatabases/validateNewDatabaseConnector.js b/backend/utils/vectordatabases/validateNewDatabaseConnector.js new file mode 100644 index 0000000..fc96121 --- /dev/null +++ b/backend/utils/vectordatabases/validateNewDatabaseConnector.js @@ -0,0 +1,66 @@ +const { + OrganizationConnection, +} = require("../../models/organizationConnection"); + +async function validateNewDatabaseConnector(organization, config) { + const { type, settings } = config; + if (!OrganizationConnection.supportedConnectors.includes(type)) + return { connector: null, error: "Unsupported vector database type." }; + + var statusCheck = { valid: false, message: null }; + if (type === "chroma") { + const { valid, message } = await validateChroma(settings); + statusCheck = { valid, message }; + } else if (type === "pinecone") { + const { valid, message } = await validatePinecone(settings); + statusCheck = { valid, message }; + } + if (!statusCheck.valid) + return { connector: null, error: statusCheck.message }; + + const connector = await OrganizationConnection.create( + organization.id, + type, + settings + ); + return { connector, error: null }; +} + +async function validateChroma({ instanceURL, authToken = null }) { + const { ChromaClient } = require("chromadb"); + const options = { path: instanceURL }; + if (!!authToken) options.fetchOptions.headers = { Authorization: authToken }; + + try { + const client = new ChromaClient(options); + await client.heartbeat(); // Will abort if no connection is possible. + return { valid: true, message: null }; + } catch (e) { + return { valid: false, message: e.message }; + } +} + +async function validatePinecone({ environment, index, apiKey }) { + const { PineconeClient } = require("@pinecone-database/pinecone"); + try { + const client = new PineconeClient(); + await client.init({ + apiKey, + environment, + }); + const { status } = await client.describeIndex({ + indexName: index, + }); + + if (!status.ready) throw new Error("Pinecode::Index not ready or found."); + return { valid: true, message: null }; + } catch (e) { + return { valid: false, message: e.message }; + } +} + +module.exports = { + validateNewDatabaseConnector, + validateChroma, + validatePinecone, +}; diff --git a/backend/utils/vectordatabases/validateUpdatedDatabaseConnector.js b/backend/utils/vectordatabases/validateUpdatedDatabaseConnector.js new file mode 100644 index 0000000..5d421c2 --- /dev/null +++ b/backend/utils/vectordatabases/validateUpdatedDatabaseConnector.js @@ -0,0 +1,34 @@ +const { + OrganizationConnection, +} = require("../../models/organizationConnection"); +const { + validateChroma, + validatePinecone, +} = require("./validateNewDatabaseConnector"); + +async function validateUpdatedDatabaseConnector(connector, config) { + const { type, settings } = config; + if (!OrganizationConnection.supportedConnectors.includes(type)) + return { connector: null, error: "Unsupported vector database type." }; + + var statusCheck = { valid: false, message: null }; + if (type === "chroma") { + const { valid, message } = await validateChroma(settings); + statusCheck = { valid, message }; + } else if (type === "pinecone") { + const { valid, message } = await validatePinecone(settings); + statusCheck = { valid, message }; + } + if (!statusCheck.valid) + return { connector: null, error: statusCheck.message }; + + const updatedConnector = await OrganizationConnection.update(connector.id, { + type, + settings: JSON.stringify(settings), + }); + return { connector: updatedConnector, error: null }; +} + +module.exports = { + validateUpdatedDatabaseConnector, +}; diff --git a/clean.sh b/clean.sh new file mode 100644 index 0000000..0497313 --- /dev/null +++ b/clean.sh @@ -0,0 +1,5 @@ +# Easily kill process on port because sometimes nodemon fails to reboot +kill -9 $(lsof -t -i tcp:5000) & +kill -9 $(lsof -t -i tcp:3001) & # if running default for MacOS Monterey +kill -9 $(lsof -t -i tcp:3355) & +kill -9 $(lsof -t -i tcp:8288) \ No newline at end of file diff --git a/devSetup.js b/devSetup.js new file mode 100644 index 0000000..7f8232e --- /dev/null +++ b/devSetup.js @@ -0,0 +1,30 @@ +const fs = require('fs'); +const path = require('path'); + +const serverEnvTemplate = path.resolve(__dirname, 'backend/.env.example'); +const serverEnv = path.resolve(__dirname, 'backend/.env.development') +const frontendEnvTemplate = path.resolve(__dirname, 'frontend/.env.example'); +const frontendEnv = path.resolve(__dirname, 'frontend/.env') +const workerEnvTemplate = path.resolve(__dirname, 'workers/.env.example'); +const workerEnv = path.resolve(__dirname, 'workers/.env') + +if (!fs.existsSync(serverEnv)) { + console.log("Copying server env file template."); + fs.writeFileSync(serverEnv, ''); + fs.copyFileSync(serverEnvTemplate, serverEnv); + console.log("Server env file created."); +} + +if (!fs.existsSync(frontendEnv)) { + console.log("Copying frontend env file template."); + fs.writeFileSync(frontendEnv, ''); + fs.copyFileSync(frontendEnvTemplate, frontendEnv); + console.log("Frontend env file created."); +} + +if (!fs.existsSync(workerEnv)) { + console.log("Copying workers env file template."); + fs.writeFileSync(workerEnv, ''); + fs.copyFileSync(workerEnvTemplate, workerEnv); + console.log("Workers env file created."); +} \ No newline at end of file diff --git a/docker/.env.example b/docker/.env.example new file mode 100644 index 0000000..1e3176a --- /dev/null +++ b/docker/.env.example @@ -0,0 +1,13 @@ +SERVER_PORT=3001 +# STORAGE_DIR="./backend/storage" + +JWT_SECRET="your-random-string-here" +SYS_EMAIL="root@example.com" +SYS_PASSWORD="hunter2" + +UID="1000" +GID="1000" + +INNGEST_EVENT_KEY="background_workers" +INNGEST_SIGNING_KEY="random-string-goes-here" +INNGEST_LANDING_PAGE="true" \ No newline at end of file diff --git a/docker/DOCKER.md b/docker/DOCKER.md new file mode 100644 index 0000000..bc6177d --- /dev/null +++ b/docker/DOCKER.md @@ -0,0 +1,33 @@ +# Running VectorAdmin in Docker + +Running VectorAdmin in Docker is the easily way to get a locally hosted option running as quickly as possible. + + +## Requirements +- Install [Docker](https://www.docker.com/) on your computer or machine. + +## How to install +- `git clone` this repo and `cd vector-admin` to get to the root directory. +- `yarn dev:setup` +- `cd docker/` +- `cp .env.example .env` to create the `.env` file. +- Edit `.env` file and update the variables. **please** update `JWT_SECRET`,`SYS_PASSWORD`, and `INNGEST_SIGNING_KEY`. +- `docker-compose up -d --build` to build the image - this will take a few moments. + +Your docker host will show the image as online once the build process is completed. This will build the app to `http://localhost:3001`. + + +## How to use the user interface +- To access the full application, visit `http://localhost:3001` in your browser. +- You first login will require you to use the `SYS_EMAIL` and `SYS_PASSWORD` set in the `.env` file. After onboarding this login will be permanently disabled. + + +# Connecting to a Vector Database + +**Pinecone** +Once your organization is connected you will need to put in your Pinecone configuration and keys. Once connected you may see a `Sync Pinecone Data` button on the organization homepage. This indicates there is existing data in your vector database that can be pulled in. If syncing, the time to sync is dependent on how many documents you have embedded in Pinecone. Otherwise, you can just create a workspace and add documents via the UI. + +**Chroma** _running locally_ +When trying to connect to a Chroma instance running also on the same machine use `http://host.docker.internal:[CHROMA_PORT]` as the URL to connect with. + +Once connected you may see a `Sync Chroma Data` button on the organization homepage. This indicates there is existing data in your vector database that can be pulled in. If syncing, the time to sync is dependent on how many documents you have embedded in Chroma. Otherwise, you can just create a workspace and add documents via the UI. diff --git a/docker/Dockerfile b/docker/Dockerfile new file mode 100644 index 0000000..5831e92 --- /dev/null +++ b/docker/Dockerfile @@ -0,0 +1,110 @@ +# Setup base image +FROM ubuntu:jammy-20230522 AS base + +# Build arguments +ARG ARG_UID +ARG ARG_GID + +# Install system dependencies +RUN DEBIAN_FRONTEND=noninteractive apt-get update && \ + DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \ + curl libgfortran5 python3 python3-pip tzdata netcat \ + libasound2 libatk1.0-0 libc6 libcairo2 libcups2 libdbus-1-3 libexpat1 libfontconfig1 \ + libgcc1 libglib2.0-0 libgtk-3-0 libnspr4 libpango-1.0-0 libx11-6 libx11-xcb1 libxcb1 \ + libxcomposite1 libxcursor1 libxdamage1 libxext6 libxfixes3 libxi6 libxrandr2 libxrender1 \ + libxss1 libxtst6 ca-certificates fonts-liberation libappindicator1 libnss3 lsb-release \ + xdg-utils && \ + curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ + apt-get install -yq --no-install-recommends nodejs && \ + curl -LO https://github.com/yarnpkg/yarn/releases/download/v1.22.19/yarn_1.22.19_all.deb \ + && dpkg -i yarn_1.22.19_all.deb \ + && rm yarn_1.22.19_all.deb && \ + curl -LO https://github.com/jgm/pandoc/releases/download/3.1.3/pandoc-3.1.3-1-amd64.deb \ + && dpkg -i pandoc-3.1.3-1-amd64.deb \ + && rm pandoc-3.1.3-1-amd64.deb && \ + rm -rf /var/lib/apt/lists/* /usr/share/icons && \ + dpkg-reconfigure -f noninteractive tzdata && \ + python3 -m pip install --no-cache-dir virtualenv + +# Create a group and user with specific UID and GID +RUN groupadd -g $ARG_GID anythingllm && \ + useradd -u $ARG_UID -m -d /app -s /bin/bash -g anythingllm anythingllm && \ + mkdir -p /app/frontend/ /app/backend/ /app/workers/ /app/document-processor/ && chown -R anythingllm:anythingllm /app + +# Copy docker helper scripts +COPY ./docker/docker-entrypoint.sh /usr/local/bin/ +COPY ./docker/docker-healthcheck.sh /usr/local/bin/ + +# Ensure the scripts are executable +RUN chmod +x /usr/local/bin/docker-entrypoint.sh && \ + chmod +x /usr/local/bin/docker-healthcheck.sh + +USER anythingllm + +WORKDIR /app + +# Install frontend dependencies +FROM base as frontend-deps +COPY ./frontend/package.json ./frontend/yarn.lock ./frontend/ +RUN cd ./frontend/ && yarn install && yarn cache clean + +# Install server dependencies +FROM base as server-deps +COPY ./backend/package.json ./backend/yarn.lock ./backend/ +RUN cd ./backend/ && yarn install --production && yarn cache clean + +# Build the frontend +FROM frontend-deps as build-stage +COPY ./frontend/ ./frontend/ +RUN cd ./frontend/ && yarn build && yarn cache clean + +# Setup the server +FROM server-deps as production-stage +COPY ./backend/ ./backend/ + +# Copy built static frontend files to the server public directory +COPY --from=build-stage /app/frontend/dist ./backend/public + +# Copy worker source files +COPY ./workers/ ./workers/ +USER root +RUN chown -R anythingllm:anythingllm ./workers +USER anythingllm + +# Install worker dependencies +RUN cd /app/workers && \ + yarn install --production && \ + yarn cache clean && \ + yarn add global inngest-cli + +# # Copy the document-processor +COPY ./document-processor/ ./document-processor/ + +# # Install document-processor dependencies +RUN cd /app/document-processor && \ + python3 -m virtualenv v-env && \ + . v-env/bin/activate && \ + pip install --no-cache-dir -r requirements.txt + +# Create placeholder database files from .placeholderdb files in /storage. +USER root +COPY ./backend/storage/job_queue.placeholderdb ./backend/storage/job_queue.db +COPY ./backend/storage/vdbms.placeholderdb ./backend/storage/vdbms.db +RUN chown -R anythingllm:anythingllm ./backend/storage ./document-processor +USER anythingllm + +# Setup the environment +ENV NODE_ENV=production +ENV PATH=/app/document-processor/v-env/bin:$PATH + +# Expose the server port +EXPOSE 3001 +EXPOSE 3355 +EXPOSE 8288 + +# Setup the healthcheck +HEALTHCHECK --interval=1m --timeout=10s --start-period=1m \ + CMD /bin/bash /usr/local/bin/docker-healthcheck.sh || exit 1 + +# Run the server +ENTRYPOINT ["/bin/bash", "/usr/local/bin/docker-entrypoint.sh"] \ No newline at end of file diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml new file mode 100644 index 0000000..1def7c7 --- /dev/null +++ b/docker/docker-compose.yml @@ -0,0 +1,26 @@ +version: '3.9' + +networks: + vector-admin: + driver: bridge + +services: + vector-admin: + container_name: vector-admin + image: vector-admin:latest + platform: linux/amd64 + build: + context: ../. + dockerfile: ./docker/Dockerfile + args: + ARG_UID: ${UID} + ARG_GID: ${GID} + user: "${UID}:${GID}" + ports: + - "3001:3001" + - "3355:3355" + - "8288:8288" + env_file: + - .env + networks: + - vector-admin diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh new file mode 100755 index 0000000..82c02f2 --- /dev/null +++ b/docker/docker-entrypoint.sh @@ -0,0 +1,9 @@ +#!/bin/bash +node /app/backend/index.js & +NODE_ENV=development node /app/workers/index.js & +{ cd /app/workers/ && NODE_ENV=development yarn run inngest-cli dev -u http://0.0.0.0:3355/background-workers; } & +{ FLASK_ENV=production FLASK_APP=wsgi.py cd document-processor && gunicorn --workers 4 --bind 0.0.0.0:8888 wsgi:api; } & +wait -n +exit $? + +yarn run inngest-cli dev -u http://127.0.0.1:3355/background-workers \ No newline at end of file diff --git a/docker/docker-healthcheck.sh b/docker/docker-healthcheck.sh new file mode 100644 index 0000000..45a8847 --- /dev/null +++ b/docker/docker-healthcheck.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Send a request to the specified URL +response=$(curl --write-out '%{http_code}' --silent --output /dev/null http://localhost:3001/api/ping) + +# If the HTTP response code is 200 (OK), the server is up +if [ $response -eq 200 ]; then + echo "Server is up" + exit 0 +else + echo "Server is down" + exit 1 +fi diff --git a/document-processor/.gitignore b/document-processor/.gitignore new file mode 100644 index 0000000..9dba3fa --- /dev/null +++ b/document-processor/.gitignore @@ -0,0 +1,6 @@ +hotdir/* +hotdir/failed/* +hotdir/processed/* +!hotdir/failed +!hotdir/processed +!hotdir/__HOTDIR__.md \ No newline at end of file diff --git a/document-processor/README.md b/document-processor/README.md new file mode 100644 index 0000000..9b40e6b --- /dev/null +++ b/document-processor/README.md @@ -0,0 +1,4 @@ + +### Running the document processing API locally +From the `collector` directory with the `v-env` active run `flask run --host '0.0.0.0' --port 8888`. +Now uploads from the frontend will be processed as if you ran the `watch.py` script manually. \ No newline at end of file diff --git a/document-processor/api.py b/document-processor/api.py new file mode 100644 index 0000000..bc45b26 --- /dev/null +++ b/document-processor/api.py @@ -0,0 +1,21 @@ +from flask import Flask, json, request +from scripts.extract_text import extract_text +from scripts.filetypes import ACCEPTED_MIMES +api = Flask(__name__) + +WATCH_DIRECTORY = "hotdir" +@api.route('/process', methods=['POST']) +def prepare_for_embed(): + content = request.json + target_filename = content.get('filename') + print(f"Processing {target_filename}") + success, reason, metadata = extract_text(WATCH_DIRECTORY, target_filename) + return json.dumps({'filename': target_filename, 'success': success, 'reason': reason, 'metadata': metadata}) + +@api.route('/accepts', methods=['GET']) +def get_accepted_filetypes(): + return json.dumps(ACCEPTED_MIMES) + +@api.route('/', methods=['GET']) +def root(): + return "

Use POST /process with filename key in JSON body in order to process a file. File by that name must exist in hotdir already.

" \ No newline at end of file diff --git a/document-processor/hotdir/__HOTDIR__.md b/document-processor/hotdir/__HOTDIR__.md new file mode 100644 index 0000000..b3a4252 --- /dev/null +++ b/document-processor/hotdir/__HOTDIR__.md @@ -0,0 +1 @@ +placeholder \ No newline at end of file diff --git a/document-processor/requirements.txt b/document-processor/requirements.txt new file mode 100644 index 0000000..1ab1d70 --- /dev/null +++ b/document-processor/requirements.txt @@ -0,0 +1,112 @@ +about-time==4.2.1 +aiohttp==3.8.4 +aiosignal==1.3.1 +alive-progress==3.1.2 +anyio==3.7.0 +appdirs==1.4.4 +argilla==1.8.0 +async-timeout==4.0.2 +attrs==23.1.0 +backoff==2.2.1 +beautifulsoup4==4.12.2 +blinker==1.6.2 +bs4==0.0.1 +certifi==2023.5.7 +cffi==1.15.1 +chardet==5.1.0 +charset-normalizer==3.1.0 +click==8.1.3 +commonmark==0.9.1 +cryptography==41.0.1 +cssselect==1.2.0 +dataclasses-json==0.5.7 +Deprecated==1.2.14 +docx2txt==0.8 +et-xmlfile==1.1.0 +exceptiongroup==1.1.1 +fake-useragent==1.1.3 +Flask==2.3.2 +frozenlist==1.3.3 +grapheme==0.6.0 +greenlet==2.0.2 +gunicorn==20.1.0 +h11==0.14.0 +httpcore==0.16.3 +httpx==0.23.3 +idna==3.4 +importlib-metadata==6.6.0 +importlib-resources==5.12.0 +inquirerpy==0.3.4 +install==1.3.5 +itsdangerous==2.1.2 +Jinja2==3.1.2 +joblib==1.2.0 +langchain==0.0.189 +lxml==4.9.2 +Markdown==3.4.3 +MarkupSafe==2.1.3 +marshmallow==3.19.0 +marshmallow-enum==1.5.1 +monotonic==1.6 +msg-parser==1.2.0 +multidict==6.0.4 +mypy-extensions==1.0.0 +nltk==3.8.1 +numexpr==2.8.4 +numpy==1.23.5 +olefile==0.46 +openapi-schema-pydantic==1.2.4 +openpyxl==3.1.2 +packaging==23.1 +pandas==1.5.3 +parse==1.19.0 +pdfminer.six==20221105 +pfzy==0.3.4 +Pillow==9.5.0 +prompt-toolkit==3.0.38 +pycparser==2.21 +pydantic==1.10.8 +pyee==8.2.2 +Pygments==2.15.1 +pypandoc==1.4 +pypdf==3.9.0 +pyppeteer==1.0.2 +pyquery==2.0.0 +python-dateutil==2.8.2 +python-docx==0.8.11 +python-dotenv==0.21.1 +python-magic==0.4.27 +python-pptx==0.6.21 +python-slugify==8.0.1 +pytz==2023.3 +PyYAML==6.0 +regex==2023.5.5 +requests==2.31.0 +requests-html==0.10.0 +rfc3986==1.5.0 +rich==13.0.1 +six==1.16.0 +sniffio==1.3.0 +soupsieve==2.4.1 +SQLAlchemy==2.0.15 +tabulate==0.9.0 +tenacity==8.2.2 +text-unidecode==1.3 +tiktoken==0.4.0 +tqdm==4.65.0 +typer==0.9.0 +typing-inspect==0.9.0 +typing_extensions==4.6.3 +unstructured==0.7.1 +urllib3==1.26.16 +uuid==1.30 +w3lib==2.1.1 +wcwidth==0.2.6 +websockets==10.4 +Werkzeug==2.3.6 +wrapt==1.14.1 +xlrd==2.0.1 +XlsxWriter==3.1.2 +yarl==1.9.2 +youtube-transcript-api==0.6.0 +zipp==3.15.0 \ No newline at end of file diff --git a/document-processor/scripts/__init__.py b/document-processor/scripts/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/document-processor/scripts/extract_text.py b/document-processor/scripts/extract_text.py new file mode 100644 index 0000000..9850a09 --- /dev/null +++ b/document-processor/scripts/extract_text.py @@ -0,0 +1,35 @@ +import os +from .filetypes import FILETYPES +from .utils import move_source + +RESERVED = ['__HOTDIR__.md'] + +# This script will do a one-off processing of a specific document that exists in hotdir. +# For this function we remove the original source document since there is no need to keep it and it will +# only occupy additional disk space. +def extract_text(directory, target_doc): + if os.path.isdir(f"{directory}/{target_doc}") or target_doc in RESERVED: return (False, "Not a file", []) + + if os.path.exists(f"{directory}/{target_doc}") is False: + print(f"{directory}/{target_doc} does not exist.") + return (False, f"{directory}/{target_doc} does not exist.", []) + + filename, fileext = os.path.splitext(target_doc) + if filename in ['.DS_Store'] or fileext == '': return False + if fileext == '.lock': + print(f"{filename} is locked - skipping until unlocked") + return (False, f"{filename} is locked - skipping until unlocked", []) + + if fileext not in FILETYPES.keys(): + print(f"{fileext} not a supported file type for conversion. It will not be processed.") + move_source(new_destination_filename=target_doc, failed=True, remove=True) + return (False, f"{fileext} not a supported file type for conversion. It will not be processed.", []) + + metadata = FILETYPES[fileext]( + directory=directory, + filename=filename, + ext=fileext, + remove_on_complete=True # remove source document to save disk space. + ) + + return (True, None, metadata) diff --git a/document-processor/scripts/filetypes.py b/document-processor/scripts/filetypes.py new file mode 100644 index 0000000..b80ba87 --- /dev/null +++ b/document-processor/scripts/filetypes.py @@ -0,0 +1,22 @@ +from .parsers.as_text import as_text +from .parsers.as_markdown import as_markdown +from .parsers.as_pdf import as_pdf +from .parsers.as_docx import as_docx, as_odt +from .parsers.as_mbox import as_mbox + +FILETYPES = { + '.txt': as_text, + '.md': as_markdown, + '.pdf': as_pdf, + '.docx': as_docx, + '.odt': as_odt, + '.mbox': as_mbox, +} + +ACCEPTED_MIMES = { + 'text/plain': ['.txt', '.md'], + 'application/vnd.openxmlformats-officedocument.wordprocessingml.document': ['.docx'], + 'application/vnd.oasis.opendocument.text': ['.odt'], + 'application/pdf': ['.pdf'], + 'application/mbox': ['.mbox'], +} diff --git a/document-processor/scripts/parsers/as_docx.py b/document-processor/scripts/parsers/as_docx.py new file mode 100644 index 0000000..29f340d --- /dev/null +++ b/document-processor/scripts/parsers/as_docx.py @@ -0,0 +1,58 @@ +import os +from langchain.document_loaders import Docx2txtLoader, UnstructuredODTLoader +from ..utils import guid, file_creation_time, move_source, tokenize + +# Process all text-related documents. +def as_docx(**kwargs): + parent_dir = kwargs.get('directory', 'hotdir') + filename = kwargs.get('filename') + ext = kwargs.get('ext', '.txt') + remove = kwargs.get('remove_on_complete', False) + fullpath = f"{parent_dir}/{filename}{ext}" + + loader = Docx2txtLoader(fullpath) + data = loader.load()[0] + content = data.page_content + + print(f"-- Working {fullpath} --") + data = { + 'id': guid(), + 'url': "file://"+os.path.abspath(f"{parent_dir}/processed/{filename}{ext}"), + 'title': f"{filename}{ext}", + 'description': "a custom file uploaded by the user.", + 'published': file_creation_time(fullpath), + 'wordCount': len(content), + 'pageContent': content, + 'token_count_estimate': len(tokenize(content)) + } + + move_source(parent_dir, f"{filename}{ext}", remove=remove) + print(f"[SUCCESS]: {filename}{ext} converted & ready for embedding.\n") + return [data] + +def as_odt(**kwargs): + parent_dir = kwargs.get('directory', 'hotdir') + filename = kwargs.get('filename') + ext = kwargs.get('ext', '.txt') + remove = kwargs.get('remove_on_complete', False) + fullpath = f"{parent_dir}/{filename}{ext}" + + loader = UnstructuredODTLoader(fullpath) + data = loader.load()[0] + content = data.page_content + + print(f"-- Working {fullpath} --") + data = { + 'id': guid(), + 'url': "file://"+os.path.abspath(f"{parent_dir}/processed/{filename}{ext}"), + 'title': f"{filename}{ext}", + 'description': "a custom file uploaded by the user.", + 'published': file_creation_time(fullpath), + 'wordCount': len(content), + 'pageContent': content, + 'token_count_estimate': len(tokenize(content)) + } + + move_source(parent_dir, f"{filename}{ext}", remove=remove) + print(f"[SUCCESS]: {filename}{ext} converted & ready for embedding.\n") + return [data] \ No newline at end of file diff --git a/document-processor/scripts/parsers/as_markdown.py b/document-processor/scripts/parsers/as_markdown.py new file mode 100644 index 0000000..77cb7e7 --- /dev/null +++ b/document-processor/scripts/parsers/as_markdown.py @@ -0,0 +1,31 @@ +import os +from langchain.document_loaders import UnstructuredMarkdownLoader +from ..utils import guid, file_creation_time, move_source, tokenize + +# Process all text-related documents. +def as_markdown(**kwargs): + parent_dir = kwargs.get('directory', 'hotdir') + filename = kwargs.get('filename') + ext = kwargs.get('ext', '.txt') + remove = kwargs.get('remove_on_complete', False) + fullpath = f"{parent_dir}/{filename}{ext}" + + loader = UnstructuredMarkdownLoader(fullpath) + data = loader.load()[0] + content = data.page_content + + print(f"-- Working {fullpath} --") + data = { + 'id': guid(), + 'url': "file://"+os.path.abspath(f"{parent_dir}/processed/{filename}{ext}"), + 'title': f"{filename}{ext}", + 'description': "a custom file uploaded by the user.", + 'published': file_creation_time(fullpath), + 'wordCount': len(content), + 'pageContent': content, + 'token_count_estimate': len(tokenize(content)) + } + + move_source(parent_dir, f"{filename}{ext}", remove=remove) + print(f"[SUCCESS]: {filename}{ext} converted & ready for embedding.\n") + return [data] \ No newline at end of file diff --git a/document-processor/scripts/parsers/as_mbox.py b/document-processor/scripts/parsers/as_mbox.py new file mode 100644 index 0000000..8e9d5bd --- /dev/null +++ b/document-processor/scripts/parsers/as_mbox.py @@ -0,0 +1,58 @@ +import os +import datetime +import email.utils +from mailbox import mbox +from slugify import slugify +from ..utils import guid, file_creation_time, move_source, tokenize +from bs4 import BeautifulSoup + +# Process all mbox-related documents. +def as_mbox(**kwargs): + parent_dir = kwargs.get('directory', 'hotdir') + filename = kwargs.get('filename') + ext = kwargs.get('ext', '.mbox') + remove = kwargs.get('remove_on_complete', False) + fullpath = f"{parent_dir}/{filename}{ext}" + + print(f"-- Working {fullpath} --") + box = mbox(fullpath) + + metadata = [] + for message in box: + content = "" + if message.is_multipart(): + for part in message.get_payload(): + if part.get_content_type() == 'text/plain': + content = part.get_payload() + elif part.get_content_type() == 'text/html': + soup = BeautifulSoup(part.get_payload(), 'html.parser') + content = soup.get_text() + else: + content = message.get_payload() + + date_tuple = email.utils.parsedate_tz(message['Date']) + if date_tuple: + local_date = datetime.datetime.fromtimestamp(email.utils.mktime_tz(date_tuple)) + date_sent = local_date.strftime("%a, %d %b %Y %H:%M:%S") + else: + date_sent = None + + data = { + 'id': guid(), + 'url': "file://"+os.path.abspath(f"{parent_dir}/processed/{slugify(filename)}-{guid()}{ext}"), + 'title': f"{filename}{ext}", + 'description': "a custom file uploaded by the user.", + 'published': file_creation_time(fullpath), + 'sender': message['From'], + 'recipient': message['To'], + 'subject': message['Subject'], + 'date_sent': date_sent, + 'wordCount': len(content), + 'pageContent': content, + 'token_count_estimate': len(tokenize(content)) + } + metadata.append(data) + + move_source(parent_dir, f"{filename}{ext}", remove=remove) + print(f"[SUCCESS]: {filename}{ext} converted & ready for embedding.\n") + return metadata diff --git a/document-processor/scripts/parsers/as_pdf.py b/document-processor/scripts/parsers/as_pdf.py new file mode 100644 index 0000000..9264d59 --- /dev/null +++ b/document-processor/scripts/parsers/as_pdf.py @@ -0,0 +1,38 @@ +import os +from langchain.document_loaders import PyPDFLoader +from slugify import slugify +from ..utils import guid, file_creation_time, write_to_server_documents, move_source, tokenize + +# Process all text-related documents. +def as_pdf(**kwargs): + parent_dir = kwargs.get('directory', 'hotdir') + filename = kwargs.get('filename') + ext = kwargs.get('ext', '.txt') + remove = kwargs.get('remove_on_complete', False) + fullpath = f"{parent_dir}/{filename}{ext}" + + loader = PyPDFLoader(fullpath) + pages = loader.load_and_split() + + print(f"-- Working {fullpath} --") + metadata = [] + for page in pages: + pg_num = page.metadata.get('page') + print(f"-- Working page {pg_num} --") + + content = page.page_content + data = { + 'id': guid(), + 'url': "file://"+os.path.abspath(f"{parent_dir}/processed/{filename}{ext}"), + 'title': f"{filename}_pg{pg_num}{ext}", + 'description': "a custom file uploaded by the user.", + 'published': file_creation_time(fullpath), + 'wordCount': len(content), + 'pageContent': content, + 'token_count_estimate': len(tokenize(content)) + } + metadata.append(data) + + move_source(parent_dir, f"{filename}{ext}", remove=remove) + print(f"[SUCCESS]: {filename}{ext} converted & ready for embedding.\n") + return metadata \ No newline at end of file diff --git a/document-processor/scripts/parsers/as_text.py b/document-processor/scripts/parsers/as_text.py new file mode 100644 index 0000000..eae6ee7 --- /dev/null +++ b/document-processor/scripts/parsers/as_text.py @@ -0,0 +1,28 @@ +import os +from slugify import slugify +from ..utils import tokenize, guid, file_creation_time, move_source + +# Process all text-related documents. +def as_text(**kwargs): + parent_dir = kwargs.get('directory', 'hotdir') + filename = kwargs.get('filename') + ext = kwargs.get('ext', '.txt') + remove = kwargs.get('remove_on_complete', False) + fullpath = f"{parent_dir}/{filename}{ext}" + content = open(fullpath).read() + + print(f"-- Working {fullpath} --") + data = { + 'id': guid(), + 'url': "file://"+os.path.abspath(f"{parent_dir}/processed/{filename}{ext}"), + 'title': f"{filename}{ext}", + 'description': "a custom file uploaded by the user.", + 'published': file_creation_time(fullpath), + 'wordCount': len(content), + 'pageContent': content, + 'token_count_estimate': len(tokenize(content)) + } + + move_source(parent_dir, f"{filename}{ext}", remove=remove) + print(f"[SUCCESS]: {filename}{ext} converted & ready for embedding.\n") + return [data] \ No newline at end of file diff --git a/document-processor/scripts/utils.py b/document-processor/scripts/utils.py new file mode 100644 index 0000000..e450de0 --- /dev/null +++ b/document-processor/scripts/utils.py @@ -0,0 +1,44 @@ +import os, json, tiktoken +from datetime import datetime +from uuid import uuid4 + +def guid(): + return str(uuid4()) + +def file_creation_time(path_to_file): + try: + if os.name == 'nt': + return datetime.fromtimestamp(os.path.getctime(path_to_file)).strftime('%Y-%m-%d %H:%M:%S') + else: + stat = os.stat(path_to_file) + return datetime.fromtimestamp(stat.st_birthtime).strftime('%Y-%m-%d %H:%M:%S') + except AttributeError: + return datetime.today().strftime('%Y-%m-%d %H:%M:%S') + +def move_source(working_dir='hotdir', new_destination_filename='', failed=False, remove=False): + if remove and os.path.exists(f"{working_dir}/{new_destination_filename}"): + print(f"{new_destination_filename} deleted from filesystem") + os.remove(f"{working_dir}/{new_destination_filename}") + return + + destination = f"{working_dir}/processed" if not failed else f"{working_dir}/failed" + if os.path.exists(destination) == False: + os.mkdir(destination) + + os.replace(f"{working_dir}/{new_destination_filename}", f"{destination}/{new_destination_filename}") + return + +def write_to_server_documents(data, filename): + destination = f"../server/storage/documents/custom-documents" + if os.path.exists(destination) == False: os.makedirs(destination) + with open(f"{destination}/{filename}.json", 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=True, indent=4) + +def tokenize(fullText): + encoder = tiktoken.encoding_for_model("text-embedding-ada-002") + return encoder.encode(fullText) + +def ada_v2_cost(tokenCount): + rate_per = 0.0004 / 1_000 # $0.0004 / 1K tokens + total = tokenCount * rate_per + return '${:,.2f}'.format(total) if total >= 0.01 else '< $0.01' \ No newline at end of file diff --git a/document-processor/wsgi.py b/document-processor/wsgi.py new file mode 100644 index 0000000..a6f402e --- /dev/null +++ b/document-processor/wsgi.py @@ -0,0 +1,4 @@ +from api import api + +if __name__ == '__main__': + api.run(debug=False) \ No newline at end of file diff --git a/frontend/.env.example b/frontend/.env.example new file mode 100644 index 0000000..c48f367 --- /dev/null +++ b/frontend/.env.example @@ -0,0 +1,2 @@ +VITE_APP_NAME="VectorAdmin" +# VITE_API_BASE= \ No newline at end of file diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000..8b151c5 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,25 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? +legacysrc diff --git a/frontend/.nvmrc b/frontend/.nvmrc new file mode 100644 index 0000000..59f4a2f --- /dev/null +++ b/frontend/.nvmrc @@ -0,0 +1 @@ +v18.13.0 \ No newline at end of file diff --git a/frontend/.prettierrc b/frontend/.prettierrc new file mode 100644 index 0000000..544138b --- /dev/null +++ b/frontend/.prettierrc @@ -0,0 +1,3 @@ +{ + "singleQuote": true +} diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..a32af8b --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,38 @@ + + + + + + + + VectorAdmin - Vector database management made easy. + + + + + + + + + + + + + + + + + + + + + +
+ + + + \ No newline at end of file diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000..bcc9c45 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,3192 @@ +{ + "name": "tailadmin-react-free", + "version": "1.0.4", + "lockfileVersion": 3, + "requires": true, + "description": "TailAdmin is a high-quality, open-source, and free Tailwind CSS admin template that is perfect for creating data-rich backends, powerful web applications and dashboard-admin projects.", + "packages": { + "": { + "name": "tailadmin-react-free", + "version": "1.0.4", + "dependencies": { + "apexcharts": "^3.37.0", + "jsvectormap": "^1.5.1", + "localforage": "^1.10.0", + "match-sorter": "^6.3.1", + "react": "^18.2.0", + "react-apexcharts": "^1.4.0", + "react-dom": "^18.2.0", + "react-router-dom": "^6.8.1", + "sort-by": "^0.0.2" + }, + "devDependencies": { + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "autoprefixer": "^10.4.13", + "file-loader": "^6.2.0", + "postcss": "^8.4.21", + "prettier": "^2.8.4", + "prettier-plugin-tailwindcss": "^0.2.2", + "tailwindcss": "^3.2.6", + "vite": "^4.1.0", + "webpack": "^5.79.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", + "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.20.14", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.20.14.tgz", + "integrity": "sha512-0YpKHD6ImkWMEINCyDAD0HLLUH/lPCefG8ld9it8DJB2wnApraKuhgYTvTY1z7UFIfBTGy5LwncZ+5HWWGbhFw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.20.12", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", + "integrity": "sha512-XsMfHovsUYHFMdrIHkZphTN/2Hzzi78R08NuHfDBehym2VsPDL6Zn/JAD/JQdnRvbSsbQc4mVaU1m6JgtTEElg==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.1.0", + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-compilation-targets": "^7.20.7", + "@babel/helper-module-transforms": "^7.20.11", + "@babel/helpers": "^7.20.7", + "@babel/parser": "^7.20.7", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.12", + "@babel/types": "^7.20.7", + "convert-source-map": "^1.7.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.2", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.20.14", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.14.tgz", + "integrity": "sha512-AEmuXHdcD3A52HHXxaTmYlb8q/xMEhoRP67B3T4Oq7lbmSoqroMZzjnGj3+i1io3pdnF8iBYVu4Ilj+c4hBxYg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.7", + "@jridgewell/gen-mapping": "^0.3.2", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", + "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.20.7.tgz", + "integrity": "sha512-4tGORmfQcrc+bvrjb5y3dG9Mx1IOZjsHqQVUz7XCNHO+iTmqxWnVg3KRygjGmpRLJGdQSKuvFinbIb0CnZwHAQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.20.5", + "@babel/helper-validator-option": "^7.18.6", + "browserslist": "^4.21.3", + "lru-cache": "^5.1.1", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.18.9", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", + "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.19.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", + "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "dev": true, + "dependencies": { + "@babel/template": "^7.18.10", + "@babel/types": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", + "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.20.11", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.20.11.tgz", + "integrity": "sha512-uRy78kN4psmji1s2QtbtcCSaj/LILFDp0f/ymhpQH5QY3nljUZCaNWz9X1dEj/8MBdBEFECs7yRhKn8i7NjZgg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-module-imports": "^7.18.6", + "@babel/helper-simple-access": "^7.20.2", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/helper-validator-identifier": "^7.19.1", + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.10", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.20.2.tgz", + "integrity": "sha512-8RvlJG2mj4huQ4pZ+rU9lqKi9ZKiRmuvGuM2HlWmkmgOhbs6zEAw6IEiJ5cQqGbDzGZOhwuOQNtZMi/ENLjZoQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.20.2.tgz", + "integrity": "sha512-+0woI/WPq59IrqDYbVGfshjT5Dmk/nnbdpcF8SnMhhXObpTq2KNBdLFRFrkVdbDOyUmHBCxzm5FHV1rACIkIbA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.20.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", + "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.19.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", + "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.19.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", + "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.18.6.tgz", + "integrity": "sha512-XO7gESt5ouv/LRJdrVjkShckw6STTaB7l9BrpBaAHDeF5YZT+01PCwmR0SJHnkW6i8OwW/EVWRShfi4j2x+KQw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.20.13.tgz", + "integrity": "sha512-nzJ0DWCL3gB5RCXbUO3KIMMsBY2Eqbx8mBpKGE/02PgyRQFcPQLbkQ1vyy596mZLaP+dAfD+R4ckASzNVmW3jg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.20.7", + "@babel/traverse": "^7.20.13", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", + "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.18.6", + "chalk": "^2.0.0", + "js-tokens": "^4.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.20.15", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.15.tgz", + "integrity": "sha512-DI4a1oZuf8wC+oAJA9RW6ga3Zbe8RZFt7kD9i4qAspz3I/yHet1VvC3DiSy/fsUvv5pvJuNPh0LPOdCcqinDPg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.18.6.tgz", + "integrity": "sha512-A0LQGx4+4Jv7u/tWzoJF7alZwnBDQd6cGLh9P+Ttk4dpiL+J5p7NSNv/9tlEFFJDq3kjxOavWmbm6t0Gk+A3Ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.19.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.19.6.tgz", + "integrity": "sha512-RpAi004QyMNisst/pvSanoRdJ4q+jMCWyk9zdw/CyLB9j8RXEahodR6l2GyttDRyEVWZtbN+TpLiHJ3t34LbsQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.19.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.13.tgz", + "integrity": "sha512-gt3PKXs0DBoL9xCvOIIZ2NEqAGZqHjAnmVbfQtB620V0uReIQutpel14KcneZuer7UioY8ALKZ7iocavvzTNFA==", + "dependencies": { + "regenerator-runtime": "^0.13.11" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", + "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.20.13", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.13.tgz", + "integrity": "sha512-kMJXfF0T6DIS9E8cgdLCSAL+cuCK+YEZHWiLK0SXpTo8YRj5lpJu3CDNKiIBCne4m9hhTIqUg6SYTAI39tAiVQ==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.18.6", + "@babel/generator": "^7.20.7", + "@babel/helper-environment-visitor": "^7.18.9", + "@babel/helper-function-name": "^7.19.0", + "@babel/helper-hoist-variables": "^7.18.6", + "@babel/helper-split-export-declaration": "^7.18.6", + "@babel/parser": "^7.20.13", + "@babel/types": "^7.20.7", + "debug": "^4.1.0", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", + "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.19.4", + "@babel/helper-validator-identifier": "^7.19.1", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.16.17.tgz", + "integrity": "sha512-N9x1CMXVhtWEAMS7pNNONyA14f71VPQN9Cnavj1XQh6T7bskqiLLrSca4O0Vr8Wdcga943eThxnVp3JLnBMYtw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.16.17.tgz", + "integrity": "sha512-MIGl6p5sc3RDTLLkYL1MyL8BMRN4tLMRCn+yRJJmEDvYZ2M7tmAf80hx1kbNEUX2KJ50RRtxZ4JHLvCfuB6kBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.16.17.tgz", + "integrity": "sha512-a3kTv3m0Ghh4z1DaFEuEDfz3OLONKuFvI4Xqczqx4BqLyuFaFkuaG4j2MtA6fuWEFeC5x9IvqnX7drmRq/fyAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.16.17.tgz", + "integrity": "sha512-/2agbUEfmxWHi9ARTX6OQ/KgXnOWfsNlTeLcoV7HSuSTv63E4DqtAc+2XqGw1KHxKMHGZgbVCZge7HXWX9Vn+w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.16.17.tgz", + "integrity": "sha512-2By45OBHulkd9Svy5IOCZt376Aa2oOkiE9QWUK9fe6Tb+WDr8hXL3dpqi+DeLiMed8tVXspzsTAvd0jUl96wmg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.16.17.tgz", + "integrity": "sha512-mt+cxZe1tVx489VTb4mBAOo2aKSnJ33L9fr25JXpqQqzbUIw/yzIzi+NHwAXK2qYV1lEFp4OoVeThGjUbmWmdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.16.17.tgz", + "integrity": "sha512-8ScTdNJl5idAKjH8zGAsN7RuWcyHG3BAvMNpKOBaqqR7EbUhhVHOqXRdL7oZvz8WNHL2pr5+eIT5c65kA6NHug==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.16.17.tgz", + "integrity": "sha512-iihzrWbD4gIT7j3caMzKb/RsFFHCwqqbrbH9SqUSRrdXkXaygSZCZg1FybsZz57Ju7N/SHEgPyaR0LZ8Zbe9gQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.16.17.tgz", + "integrity": "sha512-7S8gJnSlqKGVJunnMCrXHU9Q8Q/tQIxk/xL8BqAP64wchPCTzuM6W3Ra8cIa1HIflAvDnNOt2jaL17vaW+1V0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.16.17.tgz", + "integrity": "sha512-kiX69+wcPAdgl3Lonh1VI7MBr16nktEvOfViszBSxygRQqSpzv7BffMKRPMFwzeJGPxcio0pdD3kYQGpqQ2SSg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.16.17.tgz", + "integrity": "sha512-dTzNnQwembNDhd654cA4QhbS9uDdXC3TKqMJjgOWsC0yNCbpzfWoXdZvp0mY7HU6nzk5E0zpRGGx3qoQg8T2DQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.16.17.tgz", + "integrity": "sha512-ezbDkp2nDl0PfIUn0CsQ30kxfcLTlcx4Foz2kYv8qdC6ia2oX5Q3E/8m6lq84Dj/6b0FrkgD582fJMIfHhJfSw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.16.17.tgz", + "integrity": "sha512-dzS678gYD1lJsW73zrFhDApLVdM3cUF2MvAa1D8K8KtcSKdLBPP4zZSLy6LFZ0jYqQdQ29bjAHJDgz0rVbLB3g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.16.17.tgz", + "integrity": "sha512-ylNlVsxuFjZK8DQtNUwiMskh6nT0vI7kYl/4fZgV1llP5d6+HIeL/vmmm3jpuoo8+NuXjQVZxmKuhDApK0/cKw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.16.17.tgz", + "integrity": "sha512-gzy7nUTO4UA4oZ2wAMXPNBGTzZFP7mss3aKR2hH+/4UUkCOyqmjXiKpzGrY2TlEUhbbejzXVKKGazYcQTZWA/w==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.16.17.tgz", + "integrity": "sha512-mdPjPxfnmoqhgpiEArqi4egmBAMYvaObgn4poorpUaqmvzzbvqbowRllQ+ZgzGVMGKaPkqUmPDOOFQRUFDmeUw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.16.17.tgz", + "integrity": "sha512-/PzmzD/zyAeTUsduZa32bn0ORug+Jd1EGGAUJvqfeixoEISYpGnAezN6lnJoskauoai0Jrs+XSyvDhppCPoKOA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.16.17.tgz", + "integrity": "sha512-2yaWJhvxGEz2RiftSk0UObqJa/b+rIAjnODJgv2GbGGpRwAfpgzyrg1WLK8rqA24mfZa9GvpjLcBBg8JHkoodg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.16.17.tgz", + "integrity": "sha512-xtVUiev38tN0R3g8VhRfN7Zl42YCJvyBhRKw1RJjwE1d2emWTVToPLNEQj/5Qxc6lVFATDiy6LjVHYhIPrLxzw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.16.17.tgz", + "integrity": "sha512-ga8+JqBDHY4b6fQAmOgtJJue36scANy4l/rL97W+0wYmijhxKetzZdKOJI7olaBaMhWt8Pac2McJdZLxXWUEQw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.16.17.tgz", + "integrity": "sha512-WnsKaf46uSSF/sZhwnqE4L/F89AYNMiD4YtEcYekBt9Q7nj0DiId2XH2Ng2PHM54qi5oPrQ8luuzGszqi/veig==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.16.17.tgz", + "integrity": "sha512-y+EHuSchhL7FjHgvQL/0fnnFmO4T1bhvWANX6gcnqTjtnKWbTvUMCpGnv2+t+31d7RzyEAYAd4u2fnIhHL6N/Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", + "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.14", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.14.tgz", + "integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.17", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz", + "integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "3.1.0", + "@jridgewell/sourcemap-codec": "1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@remix-run/router": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.3.2.tgz", + "integrity": "sha512-t54ONhl/h75X94SWsHGQ4G/ZrCEguKSRQr7DrjTciJXW0YU1QhlwYeycvK5JgkzlxmvrK7wq1NB/PLtHxoiDcA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@types/eslint": { + "version": "8.37.0", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-8.37.0.tgz", + "integrity": "sha512-Piet7dG2JBuDIfohBngQ3rCt7MgO9xCO4xIMKxBThCq5PNRB91IjlJ10eJVwfoNtvTErmxLzwBZ7rHZtbOMmFQ==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.4.tgz", + "integrity": "sha512-9K4zoImiZc3HlIp6AVUDE4CWYx22a+lhSZMYNpbjW04+YF0KWj4pJXnEMjdnFTiQibFFmElcsasJXDbdI/EPhA==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", + "dev": true + }, + "node_modules/@types/json-schema": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.11.tgz", + "integrity": "sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==", + "dev": true + }, + "node_modules/@types/node": { + "version": "18.16.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.16.2.tgz", + "integrity": "sha512-GQW/JL/5Fz/0I8RpeBG9lKp0+aNcXEaVL71c0D2Q0QHDTFvlYKT7an0onCUXj85anv7b4/WesqdfchLc0jtsCg==", + "dev": true + }, + "node_modules/@types/prop-types": { + "version": "15.7.5", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.5.tgz", + "integrity": "sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.0.27", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.0.27.tgz", + "integrity": "sha512-3vtRKHgVxu3Jp9t718R9BuzoD4NcQ8YJ5XRzsSKxNDiDonD2MXIT1TmSkenxuCycZJoQT5d2vE8LwWJxBC1gmA==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "@types/scheduler": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.0.10", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.0.10.tgz", + "integrity": "sha512-E42GW/JA4Qv15wQdqJq8DL4JhNpB3prJgjgapN3qJT9K2zO5IIAQh4VXvCEDupoqAwnz0cY4RlXeC/ajX5SFHg==", + "dev": true, + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@types/scheduler": { + "version": "0.16.2", + "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", + "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==", + "dev": true + }, + "node_modules/@vitejs/plugin-react": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-3.1.0.tgz", + "integrity": "sha512-AfgcRL8ZBhAlc3BFdigClmTUMISmmzHn7sB2h9U1odvc5U/MjWXsAaz18b/WoppUTDBzxOJwo2VdClfUcItu9g==", + "dev": true, + "dependencies": { + "@babel/core": "^7.20.12", + "@babel/plugin-transform-react-jsx-self": "^7.18.6", + "@babel/plugin-transform-react-jsx-source": "^7.19.6", + "magic-string": "^0.27.0", + "react-refresh": "^0.14.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.1.0-beta.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.5.tgz", + "integrity": "sha512-LHY/GSAZZRpsNQH+/oHqhRQ5FT7eoULcBqgfyTB5nQHogFnK3/7QoN7dLnwSE/JkUAF0SrRuclT7ODqMFtWxxQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.5.tgz", + "integrity": "sha512-1j1zTIC5EZOtCplMBG/IEwLtUojtwFVwdyVMbL/hwWqbzlQoJsWCOavrdnLkemwNoC/EOwtUFch3fuo+cbcXYQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.5.tgz", + "integrity": "sha512-L65bDPmfpY0+yFrsgz8b6LhXmbbs38OnwDCf6NpnMUYqa+ENfE5Dq9E42ny0qz/PdR0LJyq/T5YijPnU8AXEpA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.5.tgz", + "integrity": "sha512-fDKo1gstwFFSfacIeH5KfwzjykIE6ldh1iH9Y/8YkAZrhmu4TctqYjSh7t0K2VyDSXOZJ1MLhht/k9IvYGcIxg==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.5.tgz", + "integrity": "sha512-DhykHXM0ZABqfIGYNv93A5KKDw/+ywBFnuWybZZWcuzWHfbp21wUfRkbtz7dMGwGgT4iXjWuhRMA2Mzod6W4WA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.5.tgz", + "integrity": "sha512-oC4Qa0bNcqnjAowFn7MPCETQgDYytpsfvz4ujZz63Zu/a/v71HeCAAmZsgZ3YVKec3zSPYytG3/PrRCqbtcAvA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.5.tgz", + "integrity": "sha512-uEoThA1LN2NA+K3B9wDo3yKlBfVtC6rh0i4/6hvbz071E8gTNZD/pT0MsBf7MeD6KbApMSkaAK0XeKyOZC7CIA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.5.tgz", + "integrity": "sha512-37aGq6qVL8A8oPbPrSGMBcp38YZFXcHfiROflJn9jxSdSMMM5dS5P/9e2/TpaJuhE+wFrbukN2WI6Hw9MH5acg==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.5.tgz", + "integrity": "sha512-ajqrRSXaTJoPW+xmkfYN6l8VIeNnR4vBOTQO9HzR7IygoCcKWkICbKFbVTNMjMgMREqXEr0+2M6zukzM47ZUfQ==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.5.tgz", + "integrity": "sha512-WiOhulHKTZU5UPlRl53gHR8OxdGsSOxqfpqWeA2FmcwBMaoEdz6b2x2si3IwC9/fSPLfe8pBMRTHVMk5nlwnFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.5.tgz", + "integrity": "sha512-C0p9D2fAu3Twwqvygvf42iGCQ4av8MFBLiTb+08SZ4cEdwzWx9QeAHDo1E2k+9s/0w1DM40oflJOpkZ8jW4HCQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/helper-wasm-section": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-opt": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5", + "@webassemblyjs/wast-printer": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.5.tgz", + "integrity": "sha512-14vteRlRjxLK9eSyYFvw1K8Vv+iPdZU0Aebk3j6oB8TQiQYuO6hj9s4d7qf6f2HJr2khzvNldAFG13CgdkAIfA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.5.tgz", + "integrity": "sha512-tcKwlIXstBQgbKy1MlbDMlXaxpucn42eb17H29rawYLxm5+MsEmgPzeCP8B1Cl69hCice8LeKgZpRUAPtqYPgw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-buffer": "1.11.5", + "@webassemblyjs/wasm-gen": "1.11.5", + "@webassemblyjs/wasm-parser": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.5.tgz", + "integrity": "sha512-SVXUIwsLQlc8srSD7jejsfTU83g7pIGr2YYNb9oHdtldSxaOhvA5xwvIiWIfcX8PlSakgqMXsLpLfbbJ4cBYew==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@webassemblyjs/helper-api-error": "1.11.5", + "@webassemblyjs/helper-wasm-bytecode": "1.11.5", + "@webassemblyjs/ieee754": "1.11.5", + "@webassemblyjs/leb128": "1.11.5", + "@webassemblyjs/utf8": "1.11.5" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.11.5", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.5.tgz", + "integrity": "sha512-f7Pq3wvg3GSPUPzR0F6bmI89Hdb+u9WXrSKc4v+N0aV0q6r42WoF92Jp2jEorBEBRoRNXgjp53nBniDXcqZYPA==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.11.5", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/acorn": { + "version": "8.8.2", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.2.tgz", + "integrity": "sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-assertions": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", + "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "dev": true, + "peerDependencies": { + "acorn": "^8" + } + }, + "node_modules/acorn-node": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", + "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", + "dev": true, + "dependencies": { + "acorn": "^7.0.0", + "acorn-walk": "^7.0.0", + "xtend": "^4.0.2" + } + }, + "node_modules/acorn-node/node_modules/acorn": { + "version": "7.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", + "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/apexcharts": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/apexcharts/-/apexcharts-3.37.0.tgz", + "integrity": "sha512-0mg1gDKUo3JG00Q//LK0jEXBS6OLjpuglqZ8ec9cqfA5oP8owopD9n5EhfARbWROb5o8GSPzFuohTJiCm2ecWw==", + "dependencies": { + "svg.draggable.js": "^2.2.2", + "svg.easing.js": "^2.0.0", + "svg.filter.js": "^2.0.2", + "svg.pathmorphing.js": "^0.1.3", + "svg.resize.js": "^1.4.3", + "svg.select.js": "^3.0.1" + } + }, + "node_modules/arg": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", + "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", + "dev": true + }, + "node_modules/autoprefixer": { + "version": "10.4.13", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.13.tgz", + "integrity": "sha512-49vKpMqcZYsJjwotvt4+h/BCjJVnhGwcLpDt5xkcaOG3eLrG/HUYLagrihYsQ+qrIBgIzX1Rw7a6L8I/ZA1Atg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + } + ], + "dependencies": { + "browserslist": "^4.21.4", + "caniuse-lite": "^1.0.30001426", + "fraction.js": "^4.2.0", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.0", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", + "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.21.5", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.21.5.tgz", + "integrity": "sha512-tUkiguQGW7S3IhB7N+c2MV/HZPSCPAAiYBZXLsBhFB/PCy6ZKKsZrmBayHV9fdGV/ARIfJ14NkxKzRDjvp7L6w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001449", + "electron-to-chromium": "^1.4.284", + "node-releases": "^2.0.8", + "update-browserslist-db": "^1.0.10" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/camelcase-css": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", + "integrity": "sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001451", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001451.tgz", + "integrity": "sha512-XY7UbUpGRatZzoRft//5xOa69/1iGJRBlrieH6QYrkKLIFn3m7OVEJ81dSrKoy2BnKsdbX5cLrOispZNYo9v2w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + } + ] + }, + "node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chokidar/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", + "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==", + "dev": true + }, + "node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/defined": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.1.tgz", + "integrity": "sha512-hsBd2qSVCRE+5PmNdHt1uzyrFu5d3RwmFDKzyNZMFq/EwDNJF7Ee5+D5oEKF0hU6LhtoUF1macFvOe4AskQC1Q==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/detective": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/detective/-/detective-5.2.1.tgz", + "integrity": "sha512-v9XE1zRnz1wRtgurGu0Bs8uHKFSTdteYZNbIPFVhUZ39L/S79ppMpdmVOZAnoz1jfEFodc48n6MX483Xo3t1yw==", + "dev": true, + "dependencies": { + "acorn-node": "^1.8.2", + "defined": "^1.0.0", + "minimist": "^1.2.6" + }, + "bin": { + "detective": "bin/detective.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/didyoumean": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", + "integrity": "sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==", + "dev": true + }, + "node_modules/dlv": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", + "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", + "dev": true + }, + "node_modules/electron-to-chromium": { + "version": "1.4.291", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.291.tgz", + "integrity": "sha512-8vk4rSMBh9LRfZKE6wcxOLmlfA4Xsa4v0RRwB6VJkAH703klC9XfZIocmTk2gLBzW31P6XbuNeMt1aB5aAu/2g==", + "dev": true + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.13.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.13.0.tgz", + "integrity": "sha512-eyV8f0y1+bzyfh8xAwW/WTSZpLbjhqc4ne9eGSH4Zo2ejdyiNG9pU6mf9DG8a7+Auk6MFTlNOT4Y2y/9k8GKVg==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/es-module-lexer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.2.1.tgz", + "integrity": "sha512-9978wrXM50Y4rTMmW5kXIC09ZdXQZqkE4mxhwkd8VbzsGkXGPgV4zWuqQJgCEzYngdo2dYDa0l8xhX4fkSwJSg==", + "dev": true + }, + "node_modules/esbuild": { + "version": "0.16.17", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.16.17.tgz", + "integrity": "sha512-G8LEkV0XzDMNwXKgM0Jwu3nY3lSTwSGY6XbxM9cr9+s0T/qSV1q1JVPBGzm3dcjhCic9+emZDmMffkwgPeOeLg==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/android-arm": "0.16.17", + "@esbuild/android-arm64": "0.16.17", + "@esbuild/android-x64": "0.16.17", + "@esbuild/darwin-arm64": "0.16.17", + "@esbuild/darwin-x64": "0.16.17", + "@esbuild/freebsd-arm64": "0.16.17", + "@esbuild/freebsd-x64": "0.16.17", + "@esbuild/linux-arm": "0.16.17", + "@esbuild/linux-arm64": "0.16.17", + "@esbuild/linux-ia32": "0.16.17", + "@esbuild/linux-loong64": "0.16.17", + "@esbuild/linux-mips64el": "0.16.17", + "@esbuild/linux-ppc64": "0.16.17", + "@esbuild/linux-riscv64": "0.16.17", + "@esbuild/linux-s390x": "0.16.17", + "@esbuild/linux-x64": "0.16.17", + "@esbuild/netbsd-x64": "0.16.17", + "@esbuild/openbsd-x64": "0.16.17", + "@esbuild/sunos-x64": "0.16.17", + "@esbuild/win32-arm64": "0.16.17", + "@esbuild/win32-ia32": "0.16.17", + "@esbuild/win32-x64": "0.16.17" + } + }, + "node_modules/escalade": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", + "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.12.tgz", + "integrity": "sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fastq": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.15.0.tgz", + "integrity": "sha512-wBrocU2LCXXa+lWBt8RoIRD89Fi8OdABODa/kEnyeyjS5aZO5/GNvI5sEINADqP/h8M29UHTHUb53sUu5Ihqdw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/file-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", + "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "schema-utils": "^3.0.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fraction.js": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz", + "integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://www.patreon.com/infusion" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", + "dev": true + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true + }, + "node_modules/has": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", + "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.1" + }, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.11.0.tgz", + "integrity": "sha512-RRjxlvLDkD1YJwDbroBHMb+cukurkDWNyHx7D3oNB5x9rb5ogcksMC5wHCadcXoo67gVr/+3GFySh3134zi6rw==", + "dev": true, + "dependencies": { + "has": "^1.0.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsvectormap": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/jsvectormap/-/jsvectormap-1.5.1.tgz", + "integrity": "sha512-Ic2O8df/gPir2tJI+tcfWuzXNE6lI4GjBiRstBEDJx7yWIqzuJn8t3+1+/M3Ug8BCxBHhRMVPeMGLFh04al93w==" + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/lilconfig": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-2.0.6.tgz", + "integrity": "sha512-9JROoBW7pobfsx+Sq2JsASvCo6Pfo6WWoUW79HuB1BCoBXD4PLWJPqDF6fNj67pqBYTbAHkE57M1kS/+L1neOg==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.27.0.tgz", + "integrity": "sha512-8UnnX2PeRAPZuN12svgR9j7M1uWMovg/CEnIwIG0LFkXSJJe4PdfUGiTGl8V9bsBHFUtfVINcSyYxd7q+kx9fA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.13" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/match-sorter": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/match-sorter/-/match-sorter-6.3.1.tgz", + "integrity": "sha512-mxybbo3pPNuA+ZuCUhm5bwNkXrJTbsk5VWbR5wiwz/GC6LIiegBGn2w3O08UG/jdbYLinw51fSQ5xNU1U3MgBw==", + "dependencies": { + "@babel/runtime": "^7.12.5", + "remove-accents": "0.4.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", + "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "dev": true, + "dependencies": { + "braces": "^3.0.2", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimist": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.7.tgz", + "integrity": "sha512-bzfL1YUZsP41gmu/qjrEk0Q6i2ix/cVeAhbCbqH9u3zYutS1cLg00qhrD0M2MVdCcx4Sc0UpP2eBWo9rotpq6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", + "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "dev": true, + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.10.tgz", + "integrity": "sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==", + "dev": true + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/picocolors": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", + "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postcss": { + "version": "8.4.21", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", + "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + } + ], + "dependencies": { + "nanoid": "^3.3.4", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-import": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-14.1.0.tgz", + "integrity": "sha512-flwI+Vgm4SElObFVPpTIT7SU7R3qk2L7PyduMcokiaVKuWv9d/U+Gm/QAd8NDLuykTWTkcrjOeD2Pp1rMeBTGw==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" + } + }, + "node_modules/postcss-js": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/postcss-js/-/postcss-js-4.0.1.tgz", + "integrity": "sha512-dDLF8pEO191hJMtlHFPRa8xsizHaM82MLfNkUHdUtVEV3tgTp5oj+8qbEqYM57SLfc74KSbw//4SeJma2LRVIw==", + "dev": true, + "dependencies": { + "camelcase-css": "^2.0.1" + }, + "engines": { + "node": "^12 || ^14 || >= 16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.4.21" + } + }, + "node_modules/postcss-load-config": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-3.1.4.tgz", + "integrity": "sha512-6DiM4E7v4coTE4uzA8U//WhtPwyhiim3eyjEMFCnUpzbrkK9wJHgKDT2mR+HbtSrd/NubVaYTOpSpjUl8NQeRg==", + "dev": true, + "dependencies": { + "lilconfig": "^2.0.5", + "yaml": "^1.10.2" + }, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": ">=8.0.9", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "postcss": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/postcss-nested": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-6.0.0.tgz", + "integrity": "sha512-0DkamqrPcmkBDsLn+vQDIrtkSbNkv5AD/M322ySo9kqFkCIYklym2xEmWkwo+Y3/qZo34tzEPNUw4y7yMCdv5w==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.10" + }, + "engines": { + "node": ">=12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "6.0.11", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.11.tgz", + "integrity": "sha512-zbARubNdogI9j7WY4nQJBiNqQf3sLS3wCP4WfOidu+p28LofJqDH1tcXypGrcmMHhDk2t9wGhCsYe/+szLTy1g==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prettier": { + "version": "2.8.4", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.8.4.tgz", + "integrity": "sha512-vIS4Rlc2FNh0BySk3Wkd6xmwxB0FpOndW5fisM5H8hsZSxU2VWVB5CWIkIjWvrHjIhxk2g3bfMKM87zNTrZddw==", + "dev": true, + "bin": { + "prettier": "bin-prettier.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-plugin-tailwindcss": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/prettier-plugin-tailwindcss/-/prettier-plugin-tailwindcss-0.2.2.tgz", + "integrity": "sha512-5RjUbWRe305pUpc48MosoIp6uxZvZxrM6GyOgsbGLTce+ehePKNm7ziW2dLG2air9aXbGuXlHVSQQw4Lbosq3w==", + "dev": true, + "engines": { + "node": ">=12.17.0" + }, + "peerDependencies": { + "@prettier/plugin-php": "*", + "@prettier/plugin-pug": "*", + "@shopify/prettier-plugin-liquid": "*", + "@shufo/prettier-plugin-blade": "*", + "@trivago/prettier-plugin-sort-imports": "*", + "prettier": ">=2.2.0", + "prettier-plugin-astro": "*", + "prettier-plugin-css-order": "*", + "prettier-plugin-import-sort": "*", + "prettier-plugin-jsdoc": "*", + "prettier-plugin-organize-attributes": "*", + "prettier-plugin-organize-imports": "*", + "prettier-plugin-style-order": "*", + "prettier-plugin-svelte": "*", + "prettier-plugin-twig-melody": "*" + }, + "peerDependenciesMeta": { + "@prettier/plugin-php": { + "optional": true + }, + "@prettier/plugin-pug": { + "optional": true + }, + "@shopify/prettier-plugin-liquid": { + "optional": true + }, + "@shufo/prettier-plugin-blade": { + "optional": true + }, + "@trivago/prettier-plugin-sort-imports": { + "optional": true + }, + "prettier-plugin-astro": { + "optional": true + }, + "prettier-plugin-css-order": { + "optional": true + }, + "prettier-plugin-import-sort": { + "optional": true + }, + "prettier-plugin-jsdoc": { + "optional": true + }, + "prettier-plugin-organize-attributes": { + "optional": true + }, + "prettier-plugin-organize-imports": { + "optional": true + }, + "prettier-plugin-style-order": { + "optional": true + }, + "prettier-plugin-svelte": { + "optional": true + }, + "prettier-plugin-twig-melody": { + "optional": true + } + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/punycode": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.0.tgz", + "integrity": "sha512-rRV+zQD8tVFys26lAGR9WUuS4iUAngJScM+ZRSKtvl5tKeZ2t5bvdNFdNHBW9FWR4guGHlgmsZ1G7BSm2wTbuA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-apexcharts": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/react-apexcharts/-/react-apexcharts-1.4.0.tgz", + "integrity": "sha512-DrcMV4aAMrUG+n6412yzyATWEyCDWlpPBBhVbpzBC4PDeuYU6iF84SmExbck+jx5MUm4U5PM3/T307Mc3kzc9Q==", + "dependencies": { + "prop-types": "^15.5.7" + }, + "peerDependencies": { + "apexcharts": "^3.18.0", + "react": ">=0.13" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-router": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router/-/react-router-6.8.1.tgz", + "integrity": "sha512-Jgi8BzAJQ8MkPt8ipXnR73rnD7EmZ0HFFb7jdQU24TynGW1Ooqin2KVDN9voSC+7xhqbbCd2cjGUepb6RObnyg==", + "dependencies": { + "@remix-run/router": "1.3.2" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8" + } + }, + "node_modules/react-router-dom": { + "version": "6.8.1", + "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.8.1.tgz", + "integrity": "sha512-67EXNfkQgf34P7+PSb6VlBuaacGhkKn3kpE51+P6zYSG2kiRoumXEL6e27zTa9+PGF2MNXbgIUHTVlleLbIcHQ==", + "dependencies": { + "@remix-run/router": "1.3.2", + "react-router": "6.8.1" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": ">=16.8", + "react-dom": ">=16.8" + } + }, + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "dependencies": { + "pify": "^2.3.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.13.11", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", + "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" + }, + "node_modules/remove-accents": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/remove-accents/-/remove-accents-0.4.2.tgz", + "integrity": "sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==" + }, + "node_modules/resolve": { + "version": "1.22.1", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz", + "integrity": "sha512-nBpuuYuY5jFsli/JIs1oldw6fOQCBioohqWZg/2hiaOybXOft4lonv85uDOKXdf8rhyK159cxU5cDcK/NKk8zw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.9.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "3.14.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-3.14.0.tgz", + "integrity": "sha512-o23sdgCLcLSe3zIplT9nQ1+r97okuaiR+vmAPZPTDYB7/f3tgWIYNyiQveMsZwshBT0is4eGax/HH83Q7CG+/Q==", + "dev": true, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=14.18.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/scheduler": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", + "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/schema-utils": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.2.tgz", + "integrity": "sha512-pvjEHOgWc9OWA/f/DE3ohBWTD6EleVLf7iFUkoSwAxttdBhB9QUebQgxER2kWueOvRJXPHNnyrvvh9eZINB8Eg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/sort-by": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/sort-by/-/sort-by-0.0.2.tgz", + "integrity": "sha512-iOX5oHA4a0eqTMFiWrHYqv924UeRKFBLhym7iwSVG37Egg2wApgZKAjyzM9WZjMwKv6+8Zi+nIaJ7FYsO9EkoA==" + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", + "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/svg.draggable.js": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/svg.draggable.js/-/svg.draggable.js-2.2.2.tgz", + "integrity": "sha512-JzNHBc2fLQMzYCZ90KZHN2ohXL0BQJGQimK1kGk6AvSeibuKcIdDX9Kr0dT9+UJ5O8nYA0RB839Lhvk4CY4MZw==", + "dependencies": { + "svg.js": "^2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.easing.js": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/svg.easing.js/-/svg.easing.js-2.0.0.tgz", + "integrity": "sha512-//ctPdJMGy22YoYGV+3HEfHbm6/69LJUTAqI2/5qBvaNHZ9uUFVC82B0Pl299HzgH13rKrBgi4+XyXXyVWWthA==", + "dependencies": { + "svg.js": ">=2.3.x" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.filter.js": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/svg.filter.js/-/svg.filter.js-2.0.2.tgz", + "integrity": "sha512-xkGBwU+dKBzqg5PtilaTb0EYPqPfJ9Q6saVldX+5vCRy31P6TlRCP3U9NxH3HEufkKkpNgdTLBJnmhDHeTqAkw==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.js": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/svg.js/-/svg.js-2.7.1.tgz", + "integrity": "sha512-ycbxpizEQktk3FYvn/8BH+6/EuWXg7ZpQREJvgacqn46gIddG24tNNe4Son6omdXCnSOaApnpZw6MPCBA1dODA==" + }, + "node_modules/svg.pathmorphing.js": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/svg.pathmorphing.js/-/svg.pathmorphing.js-0.1.3.tgz", + "integrity": "sha512-49HWI9X4XQR/JG1qXkSDV8xViuTLIWm/B/7YuQELV5KMOPtXjiwH4XPJvr/ghEDibmLQ9Oc22dpWpG0vUDDNww==", + "dependencies": { + "svg.js": "^2.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/svg.resize.js/-/svg.resize.js-1.4.3.tgz", + "integrity": "sha512-9k5sXJuPKp+mVzXNvxz7U0uC9oVMQrrf7cFsETznzUDDm0x8+77dtZkWdMfRlmbkEEYvUn9btKuZ3n41oNA+uw==", + "dependencies": { + "svg.js": "^2.6.5", + "svg.select.js": "^2.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.resize.js/node_modules/svg.select.js": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-2.1.2.tgz", + "integrity": "sha512-tH6ABEyJsAOVAhwcCjF8mw4crjXSI1aa7j2VQR8ZuJ37H2MBUbyeqYr5nEO7sSN3cy9AR9DUwNg0t/962HlDbQ==", + "dependencies": { + "svg.js": "^2.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/svg.select.js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/svg.select.js/-/svg.select.js-3.0.1.tgz", + "integrity": "sha512-h5IS/hKkuVCbKSieR9uQCj9w+zLHoPh+ce19bBYyqF53g6mnPB8sAtIbe1s9dh2S2fCmYX2xel1Ln3PJBbK4kw==", + "dependencies": { + "svg.js": "^2.6.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/tailwindcss": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.2.6.tgz", + "integrity": "sha512-BfgQWZrtqowOQMC2bwaSNe7xcIjdDEgixWGYOd6AL0CbKHJlvhfdbINeAW76l1sO+1ov/MJ93ODJ9yluRituIw==", + "dev": true, + "dependencies": { + "arg": "^5.0.2", + "chokidar": "^3.5.3", + "color-name": "^1.1.4", + "detective": "^5.2.1", + "didyoumean": "^1.2.2", + "dlv": "^1.1.3", + "fast-glob": "^3.2.12", + "glob-parent": "^6.0.2", + "is-glob": "^4.0.3", + "lilconfig": "^2.0.6", + "micromatch": "^4.0.5", + "normalize-path": "^3.0.0", + "object-hash": "^3.0.0", + "picocolors": "^1.0.0", + "postcss": "^8.0.9", + "postcss-import": "^14.1.0", + "postcss-js": "^4.0.0", + "postcss-load-config": "^3.1.4", + "postcss-nested": "6.0.0", + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0", + "quick-lru": "^5.1.1", + "resolve": "^1.22.1" + }, + "bin": { + "tailwind": "lib/cli.js", + "tailwindcss": "lib/cli.js" + }, + "engines": { + "node": ">=12.13.0" + }, + "peerDependencies": { + "postcss": "^8.0.9" + } + }, + "node_modules/tailwindcss/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/terser": { + "version": "5.17.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.17.1.tgz", + "integrity": "sha512-hVl35zClmpisy6oaoKALOpS0rDYLxRFLHhRuDlEGTKey9qHjS1w9GMORjuwIMt70Wan4lwsLYyWDVnWgF+KUEw==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.2", + "acorn": "^8.5.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.7", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.7.tgz", + "integrity": "sha512-AfKwIktyP7Cu50xNjXF/6Qb5lBNzYaWpU6YfoX3uZicTx0zTy0stDDCsvjDapKsSDvOeWo5MEq4TmdBy2cNoHw==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", + "jest-worker": "^27.4.5", + "schema-utils": "^3.1.1", + "serialize-javascript": "^6.0.1", + "terser": "^5.16.5" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.10.tgz", + "integrity": "sha512-OztqDenkfFkbSG+tRxBeAnCVPckDBcvibKd35yDONx6OU8N7sqgwc7rCbkJ/WcYtVRZ4ba68d6byhC21GFh7sQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + } + ], + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0" + }, + "bin": { + "browserslist-lint": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true + }, + "node_modules/vite": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.1.1.tgz", + "integrity": "sha512-LM9WWea8vsxhr782r9ntg+bhSFS06FJgCvvB0+8hf8UWtvaiDagKYWXndjfX6kGl74keHJUcpzrQliDXZlF5yg==", + "dev": true, + "dependencies": { + "esbuild": "^0.16.14", + "postcss": "^8.4.21", + "resolve": "^1.22.1", + "rollup": "^3.10.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + }, + "peerDependencies": { + "@types/node": ">= 14", + "less": "*", + "sass": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/watchpack": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz", + "integrity": "sha512-Lcvm7MGST/4fup+ifyKi2hjyIAwcdI4HRgtvTpIUxBRhB+RFtUh8XtDOxUfctVCnhVi+QQj49i91OyvzkJl6cg==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.81.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.81.0.tgz", + "integrity": "sha512-AAjaJ9S4hYCVODKLQTgG5p5e11hiMawBwV2v8MYLE0C/6UAGLuAF4n1qa9GOwdxnicaP+5k6M5HrLmD4+gIB8Q==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.3", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", + "acorn": "^8.7.1", + "acorn-import-assertions": "^1.7.6", + "browserslist": "^4.14.5", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.13.0", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.9", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.1.2", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.7", + "watchpack": "^2.4.0", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "engines": { + "node": ">=0.4" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..d9441fd --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,53 @@ +{ + "name": "tailadmin-react-free", + "private": true, + "version": "1.0.5", + "type": "module", + "scripts": { + "start": "vite --open", + "build": "vite build", + "preview": "vite preview", + "lint": "yarn prettier --write ./src" + }, + "overrides": { + "react-syntax-highlighter": { + "refractor": "~3.3.1" + } + }, + "dependencies": { + "@metamask/jazzicon": "^2.0.0", + "apexcharts": "^3.37.0", + "js-tiktoken": "^1.0.7", + "jsvectormap": "^1.5.1", + "localforage": "^1.10.0", + "lodash": "^4.17.21", + "match-sorter": "^6.3.1", + "moment": "^2.29.4", + "pluralize": "^8.0.0", + "react": "^18.2.0", + "react-apexcharts": "^1.4.0", + "react-code-blocks": "^0.0.9-0", + "react-dom": "^18.2.0", + "react-dropzone": "^14.2.3", + "react-feather": "^2.0.10", + "react-loading-icons": "^1.1.0", + "react-router-dom": "^6.8.1", + "sort-by": "^0.0.2", + "title-case": "^3.0.3", + "truncate": "^3.0.0", + "uuid": "^9.0.0" + }, + "devDependencies": { + "@types/react": "^18.0.27", + "@types/react-dom": "^18.0.10", + "@vitejs/plugin-react": "^3.1.0", + "autoprefixer": "^10.4.13", + "file-loader": "^6.2.0", + "postcss": "^8.4.21", + "prettier": "^2.8.4", + "prettier-plugin-tailwindcss": "^0.2.2", + "tailwindcss": "^3.2.6", + "vite": "^4.1.0", + "webpack": "^5.79.0" + } +} diff --git a/frontend/postcss.config.cjs b/frontend/postcss.config.cjs new file mode 100644 index 0000000..33ad091 --- /dev/null +++ b/frontend/postcss.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +} diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico new file mode 100644 index 0000000..6bf999e Binary files /dev/null and b/frontend/public/favicon.ico differ diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx new file mode 100644 index 0000000..8d83674 --- /dev/null +++ b/frontend/src/App.tsx @@ -0,0 +1,86 @@ +import { lazy, Suspense } from 'react'; +import { Route, Routes } from 'react-router-dom'; +import { ContextWrapper } from './AuthContext'; +import PrivateRoute from './components/PrivateRoute'; + +import SignIn from './pages/Authentication/SignIn'; +import SignUp from './pages/Authentication/SignUp'; +import { FullScreenLoader } from './components/Preloader'; +import UserManagementView from './pages/UsersView'; + +const OnboardingHome = lazy(() => import('./pages/Onboarding')); +const OrganizationDashboard = lazy(() => import('./pages/Dashboard')); +const WorkspaceDashboard = lazy(() => import('./pages/WorkspaceDashboard')); +const DocumentView = lazy(() => import('./pages/DocumentView')); +const SystemSetup = lazy(() => import('./pages/Authentication/SystemSetup')); +const OnboardingSecuritySetup = lazy( + () => import('./pages/Onboarding/security') +); +const OrganizationJobsView = lazy(() => import('./pages/Jobs')); +const SystemSettingsView = lazy(() => import('./pages/SystemSettings')); + +function App() { + return ( + + }> + + } /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + + } + /> + } + /> + + } /> + } /> + } /> + } + /> + + } + /> + + + + ); +} + +const Redirect = ({ to }: { to: any }) => { + if (!!window?.location) window.location = to; + return ; +}; + +export default App; diff --git a/frontend/src/AuthContext.tsx b/frontend/src/AuthContext.tsx new file mode 100644 index 0000000..16a7b51 --- /dev/null +++ b/frontend/src/AuthContext.tsx @@ -0,0 +1,31 @@ +import { useState, createContext } from 'react'; +import { STORE_TOKEN, STORE_USER } from './utils/constants'; + +export const AuthContext = createContext(null); +export function ContextWrapper(props: any) { + const localUser = localStorage.getItem(STORE_USER); + const localAuthToken = localStorage.getItem(STORE_TOKEN); + const [store, setStore] = useState({ + user: localUser ? JSON.parse(localUser) : null, + authToken: localAuthToken ? localAuthToken : null, + }); + + const [actions] = useState({ + updateUser: (user: object, authToken = '') => { + localStorage.setItem(STORE_USER, JSON.stringify(user)); + localStorage.setItem(STORE_TOKEN, authToken); + setStore({ user, authToken }); + }, + unsetUser: () => { + localStorage.removeItem(STORE_USER); + localStorage.removeItem(STORE_TOKEN); + setStore({ user: null, authToken: null }); + }, + }); + + return ( + + {props.children} + + ); +} diff --git a/frontend/src/components/Breadcrumb.tsx b/frontend/src/components/Breadcrumb.tsx new file mode 100644 index 0000000..3e3a613 --- /dev/null +++ b/frontend/src/components/Breadcrumb.tsx @@ -0,0 +1,24 @@ +import { Link } from 'react-router-dom'; +interface BreadcrumbProps { + pageName: string; +} +const Breadcrumb = ({ pageName }: BreadcrumbProps) => { + return ( +
+

+ {pageName} +

+ + +
+ ); +}; + +export default Breadcrumb; diff --git a/frontend/src/components/Header.tsx b/frontend/src/components/Header.tsx new file mode 100644 index 0000000..355230c --- /dev/null +++ b/frontend/src/components/Header.tsx @@ -0,0 +1,126 @@ +import { Link } from 'react-router-dom'; +import Logo from '../images/logo/logo-icon.png'; +import { CheckCircle, Copy } from 'react-feather'; +import { useEffect, useState } from 'react'; +import paths from '../utils/paths'; +import { STORE_TOKEN, STORE_USER } from '../utils/constants'; +import truncate from 'truncate'; + +const Header = (props: { + entity?: any | null; + property?: string; + nameProp?: string; + sidebarOpen: string | boolean | undefined; + setSidebarOpen: (arg0: boolean) => void; + extendedItems?: any; +}) => { + const [copied, setCopied] = useState(false); + if (!props.entity) return null; + const { entity, property, nameProp, extendedItems = <> } = props; + + const handleCopy = () => { + window.navigator.clipboard.writeText(entity[property]); + setCopied(true); + }; + + useEffect(() => { + function manageCopy() { + if (!copied) return false; + setTimeout(() => { + setCopied(false); + }, 1500); + } + manageCopy(); + }, [copied]); + + return ( +
+
+
+ {/* */} + + {/* */} + + + Logo + +
+ +
+
+
+

+ {truncate(entity[nameProp ?? 'name'], 20)} +

+ + {extendedItems} +
+ +
+
+
+
+ ); +}; + +export default Header; diff --git a/frontend/src/components/Jazzicon/index.tsx b/frontend/src/components/Jazzicon/index.tsx new file mode 100644 index 0000000..b555af5 --- /dev/null +++ b/frontend/src/components/Jazzicon/index.tsx @@ -0,0 +1,29 @@ +import { useRef, useEffect } from 'react'; +import JAZZ from '@metamask/jazzicon'; + +export default function Jazzicon({ size = 10, uid = '' }) { + const divRef = useRef(null); + const seed = uid + ? toPseudoRandomInteger(uid) + : Math.floor(100000 + Math.random() * 900000); + const result = JAZZ(size, seed); + + useEffect(() => { + function add() { + if (!divRef || !divRef.current) return null; + divRef?.current?.appendChild(result); + } + add(); + }, []); + + return
; +} + +function toPseudoRandomInteger(uidString = '') { + var numberArray = [uidString.length]; + for (var i = 0; i < uidString.length; i++) { + numberArray[i] = uidString.charCodeAt(i); + } + + return numberArray.reduce((a, b) => a + b, 0); +} diff --git a/frontend/src/components/Preloader.tsx b/frontend/src/components/Preloader.tsx new file mode 100644 index 0000000..6201d91 --- /dev/null +++ b/frontend/src/components/Preloader.tsx @@ -0,0 +1,16 @@ +export default function PreLoader() { + return ( +
+ ); +} + +export function FullScreenLoader() { + return ( +
+
+
+ ); +} diff --git a/frontend/src/components/PrivateRoute/index.tsx b/frontend/src/components/PrivateRoute/index.tsx new file mode 100644 index 0000000..2777051 --- /dev/null +++ b/frontend/src/components/PrivateRoute/index.tsx @@ -0,0 +1,47 @@ +import { useEffect, useState } from 'react'; +import { Navigate } from 'react-router-dom'; +import { FullScreenLoader } from '../Preloader'; +import validateSessionTokenForUser from '../../utils/session'; +import paths from '../../utils/paths'; +import { STORE_TOKEN, STORE_USER } from '../../utils/constants'; + +function useIsAuthenticated() { + const [isAuthd, setIsAuthed] = useState(null); + + useEffect(() => { + const validateSession = async () => { + const localUser = localStorage.getItem(STORE_USER); + const localAuthToken = localStorage.getItem(STORE_TOKEN); + if (!localUser || !localAuthToken) { + setIsAuthed(false); + return; + } + + const isValid = await validateSessionTokenForUser(); + if (!isValid) { + localStorage.removeItem(STORE_USER); + localStorage.removeItem(STORE_TOKEN); + setIsAuthed(false); + return; + } + + setIsAuthed(true); + }; + validateSession(); + }, []); + + return isAuthd; +} + +const PrivateRoute = ({ + Component, +}: { + Component: React.FunctionComponent; +}) => { + const authed = useIsAuthenticated(); + if (authed === null) return ; + + return authed ? : ; +}; + +export default PrivateRoute; diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx new file mode 100644 index 0000000..33e7ada --- /dev/null +++ b/frontend/src/components/Sidebar.tsx @@ -0,0 +1,412 @@ +import React, { useEffect, useRef, useState } from 'react'; +import { NavLink, useLocation, useParams } from 'react-router-dom'; +import Logo from '../images/logo/logo.png'; +import SidebarLinkGroup from './SidebarLinkGroup'; +import paths from '../utils/paths'; +import { Box, ChevronUp, Command, Radio, Tool, Users } from 'react-feather'; +import Organization from '../models/organization'; +import PreLoader from './Preloader'; +import useUser from '../hooks/useUser'; + +interface SidebarProps { + organization: any; + organizations: any[]; + workspaces: object[]; + sidebarOpen: boolean; + setSidebarOpen: (arg: boolean) => void; +} + +const Sidebar = ({ + organization, + organizations, + workspaces, + sidebarOpen, + setSidebarOpen, +}: SidebarProps) => { + const { user } = useUser(); + const { slug } = useParams(); + const location = useLocation(); + const { pathname } = location; + + const trigger = useRef(null); + const sidebar = useRef(null); + + const storedSidebarExpanded = localStorage.getItem('sidebar-expanded'); + const [sidebarExpanded, setSidebarExpanded] = useState( + storedSidebarExpanded === null ? false : storedSidebarExpanded === 'true' + ); + + // close on click outside + useEffect(() => { + const clickHandler = ({ target }: MouseEvent) => { + if (!sidebar.current || !trigger.current) return; + if ( + !sidebarOpen || + sidebar.current.contains(target) || + trigger.current.contains(target) + ) + return; + setSidebarOpen(false); + }; + document.addEventListener('click', clickHandler); + return () => document.removeEventListener('click', clickHandler); + }); + + // close if the esc key is pressed + useEffect(() => { + const keyHandler = ({ keyCode }: KeyboardEvent) => { + if (!sidebarOpen || keyCode !== 27) return; + setSidebarOpen(false); + }; + document.addEventListener('keydown', keyHandler); + return () => document.removeEventListener('keydown', keyHandler); + }); + + useEffect(() => { + localStorage.setItem('sidebar-expanded', sidebarExpanded.toString()); + if (sidebarExpanded) { + document.querySelector('body')?.classList.add('sidebar-expanded'); + } else { + document.querySelector('body')?.classList.remove('sidebar-expanded'); + } + }, [sidebarExpanded]); + + return ( + <> + + + + ); +}; + +export default Sidebar; + +const CreateOrganizationModal = () => { + const [loading, setLoading] = useState(false); + const handleSubmit = async (e: any) => { + e.preventDefault(); + setLoading(true); + const { organization } = await Organization.create(e.target.name.value); + if (!organization) { + setLoading(false); + return false; + } + + window.location.replace(paths.organization(organization)); + }; + + return ( + +
+
+

+ Create a New Organization +

+
+ {loading ? ( +
+
+ +
+
+ ) : ( +
+
+
+ + +
+
+ + +
+

+ Once your organization exists you can start workspaces and + documents. +
+ YOU CANNOT DELETE ORGANIZATIONS. +

+
+
+ )} +
+
+ ); +}; diff --git a/frontend/src/components/SidebarLinkGroup.tsx b/frontend/src/components/SidebarLinkGroup.tsx new file mode 100644 index 0000000..5330b12 --- /dev/null +++ b/frontend/src/components/SidebarLinkGroup.tsx @@ -0,0 +1,21 @@ +import { ReactNode, useState } from 'react'; + +interface SidebarLinkGroupProps { + children: (handleClick: () => void, open: boolean) => ReactNode; + activeCondition: boolean; +} + +const SidebarLinkGroup = ({ + children, + activeCondition, +}: SidebarLinkGroupProps) => { + const [open, setOpen] = useState(activeCondition); + + const handleClick = () => { + setOpen(!open); + }; + + return
  • {children(handleClick, open)}
  • ; +}; + +export default SidebarLinkGroup; diff --git a/frontend/src/fonts/Satoshi-Black.eot b/frontend/src/fonts/Satoshi-Black.eot new file mode 100644 index 0000000..11747f3 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Black.eot differ diff --git a/frontend/src/fonts/Satoshi-Black.ttf b/frontend/src/fonts/Satoshi-Black.ttf new file mode 100644 index 0000000..62015ac Binary files /dev/null and b/frontend/src/fonts/Satoshi-Black.ttf differ diff --git a/frontend/src/fonts/Satoshi-Black.woff b/frontend/src/fonts/Satoshi-Black.woff new file mode 100644 index 0000000..a6bee36 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Black.woff differ diff --git a/frontend/src/fonts/Satoshi-Black.woff2 b/frontend/src/fonts/Satoshi-Black.woff2 new file mode 100644 index 0000000..64492d5 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Black.woff2 differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.eot b/frontend/src/fonts/Satoshi-BlackItalic.eot new file mode 100644 index 0000000..de2edbb Binary files /dev/null and b/frontend/src/fonts/Satoshi-BlackItalic.eot differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.ttf b/frontend/src/fonts/Satoshi-BlackItalic.ttf new file mode 100644 index 0000000..74410b9 Binary files /dev/null and b/frontend/src/fonts/Satoshi-BlackItalic.ttf differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.woff b/frontend/src/fonts/Satoshi-BlackItalic.woff new file mode 100644 index 0000000..0e07e1c Binary files /dev/null and b/frontend/src/fonts/Satoshi-BlackItalic.woff differ diff --git a/frontend/src/fonts/Satoshi-BlackItalic.woff2 b/frontend/src/fonts/Satoshi-BlackItalic.woff2 new file mode 100644 index 0000000..9d5c911 Binary files /dev/null and b/frontend/src/fonts/Satoshi-BlackItalic.woff2 differ diff --git a/frontend/src/fonts/Satoshi-Bold.eot b/frontend/src/fonts/Satoshi-Bold.eot new file mode 100644 index 0000000..390ae25 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Bold.eot differ diff --git a/frontend/src/fonts/Satoshi-Bold.ttf b/frontend/src/fonts/Satoshi-Bold.ttf new file mode 100644 index 0000000..00bc985 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Bold.ttf differ diff --git a/frontend/src/fonts/Satoshi-Bold.woff b/frontend/src/fonts/Satoshi-Bold.woff new file mode 100644 index 0000000..bba8257 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Bold.woff differ diff --git a/frontend/src/fonts/Satoshi-Bold.woff2 b/frontend/src/fonts/Satoshi-Bold.woff2 new file mode 100644 index 0000000..0a8db7a Binary files /dev/null and b/frontend/src/fonts/Satoshi-Bold.woff2 differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.eot b/frontend/src/fonts/Satoshi-BoldItalic.eot new file mode 100644 index 0000000..426be2a Binary files /dev/null and b/frontend/src/fonts/Satoshi-BoldItalic.eot differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.ttf b/frontend/src/fonts/Satoshi-BoldItalic.ttf new file mode 100644 index 0000000..24f012c Binary files /dev/null and b/frontend/src/fonts/Satoshi-BoldItalic.ttf differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.woff b/frontend/src/fonts/Satoshi-BoldItalic.woff new file mode 100644 index 0000000..8bcb7a6 Binary files /dev/null and b/frontend/src/fonts/Satoshi-BoldItalic.woff differ diff --git a/frontend/src/fonts/Satoshi-BoldItalic.woff2 b/frontend/src/fonts/Satoshi-BoldItalic.woff2 new file mode 100644 index 0000000..225527f Binary files /dev/null and b/frontend/src/fonts/Satoshi-BoldItalic.woff2 differ diff --git a/frontend/src/fonts/Satoshi-Italic.eot b/frontend/src/fonts/Satoshi-Italic.eot new file mode 100644 index 0000000..64039a8 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Italic.eot differ diff --git a/frontend/src/fonts/Satoshi-Italic.ttf b/frontend/src/fonts/Satoshi-Italic.ttf new file mode 100644 index 0000000..c214f4f Binary files /dev/null and b/frontend/src/fonts/Satoshi-Italic.ttf differ diff --git a/frontend/src/fonts/Satoshi-Italic.woff b/frontend/src/fonts/Satoshi-Italic.woff new file mode 100644 index 0000000..edd4d93 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Italic.woff differ diff --git a/frontend/src/fonts/Satoshi-Italic.woff2 b/frontend/src/fonts/Satoshi-Italic.woff2 new file mode 100644 index 0000000..8b98599 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Italic.woff2 differ diff --git a/frontend/src/fonts/Satoshi-Light.eot b/frontend/src/fonts/Satoshi-Light.eot new file mode 100644 index 0000000..d8fcacc Binary files /dev/null and b/frontend/src/fonts/Satoshi-Light.eot differ diff --git a/frontend/src/fonts/Satoshi-Light.ttf b/frontend/src/fonts/Satoshi-Light.ttf new file mode 100644 index 0000000..b41a2d4 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Light.ttf differ diff --git a/frontend/src/fonts/Satoshi-Light.woff b/frontend/src/fonts/Satoshi-Light.woff new file mode 100644 index 0000000..8f05e4e Binary files /dev/null and b/frontend/src/fonts/Satoshi-Light.woff differ diff --git a/frontend/src/fonts/Satoshi-Light.woff2 b/frontend/src/fonts/Satoshi-Light.woff2 new file mode 100644 index 0000000..cf18cd4 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Light.woff2 differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.eot b/frontend/src/fonts/Satoshi-LightItalic.eot new file mode 100644 index 0000000..e34a0df Binary files /dev/null and b/frontend/src/fonts/Satoshi-LightItalic.eot differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.ttf b/frontend/src/fonts/Satoshi-LightItalic.ttf new file mode 100644 index 0000000..08f5db5 Binary files /dev/null and b/frontend/src/fonts/Satoshi-LightItalic.ttf differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.woff b/frontend/src/fonts/Satoshi-LightItalic.woff new file mode 100644 index 0000000..a03a50d Binary files /dev/null and b/frontend/src/fonts/Satoshi-LightItalic.woff differ diff --git a/frontend/src/fonts/Satoshi-LightItalic.woff2 b/frontend/src/fonts/Satoshi-LightItalic.woff2 new file mode 100644 index 0000000..6bd15ad Binary files /dev/null and b/frontend/src/fonts/Satoshi-LightItalic.woff2 differ diff --git a/frontend/src/fonts/Satoshi-Medium.eot b/frontend/src/fonts/Satoshi-Medium.eot new file mode 100644 index 0000000..83cacec Binary files /dev/null and b/frontend/src/fonts/Satoshi-Medium.eot differ diff --git a/frontend/src/fonts/Satoshi-Medium.ttf b/frontend/src/fonts/Satoshi-Medium.ttf new file mode 100644 index 0000000..ab149b7 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Medium.ttf differ diff --git a/frontend/src/fonts/Satoshi-Medium.woff b/frontend/src/fonts/Satoshi-Medium.woff new file mode 100644 index 0000000..cef3226 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Medium.woff differ diff --git a/frontend/src/fonts/Satoshi-Medium.woff2 b/frontend/src/fonts/Satoshi-Medium.woff2 new file mode 100644 index 0000000..ffd0ac9 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Medium.woff2 differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.eot b/frontend/src/fonts/Satoshi-MediumItalic.eot new file mode 100644 index 0000000..25d229a Binary files /dev/null and b/frontend/src/fonts/Satoshi-MediumItalic.eot differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.ttf b/frontend/src/fonts/Satoshi-MediumItalic.ttf new file mode 100644 index 0000000..387f278 Binary files /dev/null and b/frontend/src/fonts/Satoshi-MediumItalic.ttf differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.woff b/frontend/src/fonts/Satoshi-MediumItalic.woff new file mode 100644 index 0000000..46d8995 Binary files /dev/null and b/frontend/src/fonts/Satoshi-MediumItalic.woff differ diff --git a/frontend/src/fonts/Satoshi-MediumItalic.woff2 b/frontend/src/fonts/Satoshi-MediumItalic.woff2 new file mode 100644 index 0000000..212adc9 Binary files /dev/null and b/frontend/src/fonts/Satoshi-MediumItalic.woff2 differ diff --git a/frontend/src/fonts/Satoshi-Regular.eot b/frontend/src/fonts/Satoshi-Regular.eot new file mode 100644 index 0000000..452666f Binary files /dev/null and b/frontend/src/fonts/Satoshi-Regular.eot differ diff --git a/frontend/src/fonts/Satoshi-Regular.ttf b/frontend/src/fonts/Satoshi-Regular.ttf new file mode 100644 index 0000000..fe85cd6 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Regular.ttf differ diff --git a/frontend/src/fonts/Satoshi-Regular.woff b/frontend/src/fonts/Satoshi-Regular.woff new file mode 100644 index 0000000..03ac195 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Regular.woff differ diff --git a/frontend/src/fonts/Satoshi-Regular.woff2 b/frontend/src/fonts/Satoshi-Regular.woff2 new file mode 100644 index 0000000..81c40ab Binary files /dev/null and b/frontend/src/fonts/Satoshi-Regular.woff2 differ diff --git a/frontend/src/fonts/Satoshi-Variable.eot b/frontend/src/fonts/Satoshi-Variable.eot new file mode 100644 index 0000000..f42624e Binary files /dev/null and b/frontend/src/fonts/Satoshi-Variable.eot differ diff --git a/frontend/src/fonts/Satoshi-Variable.ttf b/frontend/src/fonts/Satoshi-Variable.ttf new file mode 100644 index 0000000..976e85c Binary files /dev/null and b/frontend/src/fonts/Satoshi-Variable.ttf differ diff --git a/frontend/src/fonts/Satoshi-Variable.woff b/frontend/src/fonts/Satoshi-Variable.woff new file mode 100644 index 0000000..f8dcd1d Binary files /dev/null and b/frontend/src/fonts/Satoshi-Variable.woff differ diff --git a/frontend/src/fonts/Satoshi-Variable.woff2 b/frontend/src/fonts/Satoshi-Variable.woff2 new file mode 100644 index 0000000..b00e833 Binary files /dev/null and b/frontend/src/fonts/Satoshi-Variable.woff2 differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.eot b/frontend/src/fonts/Satoshi-VariableItalic.eot new file mode 100644 index 0000000..5f4554a Binary files /dev/null and b/frontend/src/fonts/Satoshi-VariableItalic.eot differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.ttf b/frontend/src/fonts/Satoshi-VariableItalic.ttf new file mode 100644 index 0000000..4c2677c Binary files /dev/null and b/frontend/src/fonts/Satoshi-VariableItalic.ttf differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.woff b/frontend/src/fonts/Satoshi-VariableItalic.woff new file mode 100644 index 0000000..3fe029e Binary files /dev/null and b/frontend/src/fonts/Satoshi-VariableItalic.woff differ diff --git a/frontend/src/fonts/Satoshi-VariableItalic.woff2 b/frontend/src/fonts/Satoshi-VariableItalic.woff2 new file mode 100644 index 0000000..e7ab3a0 Binary files /dev/null and b/frontend/src/fonts/Satoshi-VariableItalic.woff2 differ diff --git a/frontend/src/hooks/useColorMode.tsx b/frontend/src/hooks/useColorMode.tsx new file mode 100644 index 0000000..02eaf8e --- /dev/null +++ b/frontend/src/hooks/useColorMode.tsx @@ -0,0 +1,19 @@ +import { useEffect } from 'react'; +import useLocalStorage from './useLocalStorage'; + +const useColorMode = () => { + const [colorMode, setColorMode] = useLocalStorage('color-theme', 'light'); + + useEffect(() => { + const className = 'dark'; + const bodyClass = window.document.body.classList; + + colorMode === 'dark' + ? bodyClass.add(className) + : bodyClass.remove(className); + }, [colorMode]); + + return [colorMode, setColorMode]; +}; + +export default useColorMode; diff --git a/frontend/src/hooks/useLocalStorage.tsx b/frontend/src/hooks/useLocalStorage.tsx new file mode 100644 index 0000000..68492a8 --- /dev/null +++ b/frontend/src/hooks/useLocalStorage.tsx @@ -0,0 +1,43 @@ +import { useEffect, useState } from 'react'; + +type SetValue = T | ((val: T) => T); + +function useLocalStorage( + key: string, + initialValue: T +): [T, (value: SetValue) => void] { + // State to store our value + // Pass initial state function to useState so logic is only executed once + const [storedValue, setStoredValue] = useState(() => { + try { + // Get from local storage by key + const item = window.localStorage.getItem(key); + // Parse stored json or if none return initialValue + return item ? JSON.parse(item) : initialValue; + } catch (error) { + // If error also return initialValue + console.log(error); + return initialValue; + } + }); + + // useEffect to update local storage when the state changes + useEffect(() => { + try { + // Allow value to be a function so we have same API as useState + const valueToStore = + typeof storedValue === 'function' + ? storedValue(storedValue) + : storedValue; + // Save state + window.localStorage.setItem(key, JSON.stringify(valueToStore)); + } catch (error) { + // A more advanced implementation would handle the error case + console.log(error); + } + }, [key, storedValue]); + + return [storedValue, setStoredValue]; +} + +export default useLocalStorage; diff --git a/frontend/src/hooks/useQuery.tsx b/frontend/src/hooks/useQuery.tsx new file mode 100644 index 0000000..2af24ed --- /dev/null +++ b/frontend/src/hooks/useQuery.tsx @@ -0,0 +1,3 @@ +export default function useQuery() { + return new URLSearchParams(window.location.search); +} diff --git a/frontend/src/hooks/useUser.tsx b/frontend/src/hooks/useUser.tsx new file mode 100644 index 0000000..a39737f --- /dev/null +++ b/frontend/src/hooks/useUser.tsx @@ -0,0 +1,20 @@ +import { useContext } from 'react'; +import { AuthContext } from '../AuthContext'; + +interface IContext { + store: { + user: { + uid: string; + name: string | null; + email: string; + stytchUserId: string | null; + stytchEmailId: string | null; + }; + }; +} + +export default function useUser() { + const context = useContext(AuthContext as any); + + return { ...context.store }; +} diff --git a/frontend/src/images/logo/logo-dark.png b/frontend/src/images/logo/logo-dark.png new file mode 100644 index 0000000..419b738 Binary files /dev/null and b/frontend/src/images/logo/logo-dark.png differ diff --git a/frontend/src/images/logo/logo-icon.png b/frontend/src/images/logo/logo-icon.png new file mode 100644 index 0000000..1d9cc38 Binary files /dev/null and b/frontend/src/images/logo/logo-icon.png differ diff --git a/frontend/src/images/logo/logo.png b/frontend/src/images/logo/logo.png new file mode 100644 index 0000000..1d9cc38 Binary files /dev/null and b/frontend/src/images/logo/logo.png differ diff --git a/frontend/src/images/logo/logo.svg b/frontend/src/images/logo/logo.svg new file mode 100644 index 0000000..2d492d1 --- /dev/null +++ b/frontend/src/images/logo/logo.svg @@ -0,0 +1,3 @@ +VectorAdmin +vector data made easy + \ No newline at end of file diff --git a/frontend/src/images/undraws/manage.svg b/frontend/src/images/undraws/manage.svg new file mode 100644 index 0000000..65c0af4 --- /dev/null +++ b/frontend/src/images/undraws/manage.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/src/images/vectordbs/chroma.png b/frontend/src/images/vectordbs/chroma.png new file mode 100644 index 0000000..fde2538 Binary files /dev/null and b/frontend/src/images/vectordbs/chroma.png differ diff --git a/frontend/src/images/vectordbs/pinecone.png b/frontend/src/images/vectordbs/pinecone.png new file mode 100644 index 0000000..9f20d7f Binary files /dev/null and b/frontend/src/images/vectordbs/pinecone.png differ diff --git a/frontend/src/index.css b/frontend/src/index.css new file mode 100644 index 0000000..05cb4c0 --- /dev/null +++ b/frontend/src/index.css @@ -0,0 +1,35 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +/* Chrome, Safari and Opera */ +.no-scrollbar::-webkit-scrollbar { + display: none; +} + +.no-scrollbar { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +dialog { + pointer-events: none; + opacity: 0; + transition: opacity 0.2s; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; +} + +dialog[open] { + opacity: 1; + pointer-events: inherit; +} + +dialog::backdrop { + background: rgba(0, 0, 0, 0.5); + backdrop-filter: blur(2px); +} diff --git a/frontend/src/layout/AppLayout.tsx b/frontend/src/layout/AppLayout.tsx new file mode 100644 index 0000000..31d75fd --- /dev/null +++ b/frontend/src/layout/AppLayout.tsx @@ -0,0 +1,63 @@ +import { ReactNode, useState } from 'react'; +import Header from '../components/Header'; +import Sidebar from '../components/Sidebar'; + +interface DefaultLayoutProps { + headerEntity: any; + headerProp: string; + headerNameProp?: string; + workspaces: any[]; + organizations: any[]; + organization: any; + headerExtendedItems?: ReactNode; + children: ReactNode; +} + +const AppLayout = ({ + workspaces, + headerEntity, + headerProp, + headerNameProp, + organizations, + organization, + headerExtendedItems, + children, +}: DefaultLayoutProps) => { + const [sidebarOpen, setSidebarOpen] = useState(false); + + return ( +
    +
    + + +
    + {!!headerEntity && ( +
    +
    +
    + )} +
    +
    + {children} +
    +
    +
    +
    +
    + ); +}; + +export default AppLayout; diff --git a/frontend/src/layout/DefaultLayout.tsx b/frontend/src/layout/DefaultLayout.tsx new file mode 100644 index 0000000..ee40575 --- /dev/null +++ b/frontend/src/layout/DefaultLayout.tsx @@ -0,0 +1,23 @@ +import { ReactNode } from 'react'; + +interface DefaultLayoutProps { + children: ReactNode; +} + +const DefaultLayout = ({ children }: DefaultLayoutProps) => { + return ( +
    +
    +
    +
    +
    + {children} +
    +
    +
    +
    +
    + ); +}; + +export default DefaultLayout; diff --git a/frontend/src/lib.d.ts b/frontend/src/lib.d.ts new file mode 100644 index 0000000..091d25e --- /dev/null +++ b/frontend/src/lib.d.ts @@ -0,0 +1,4 @@ +declare module '*.svg' { + const content: any; + export default content; +} diff --git a/frontend/src/main.tsx b/frontend/src/main.tsx new file mode 100644 index 0000000..9772ace --- /dev/null +++ b/frontend/src/main.tsx @@ -0,0 +1,11 @@ +import ReactDOM from 'react-dom/client'; +import { BrowserRouter as Router } from 'react-router-dom'; +import App from './App'; +import './index.css'; +import './satoshi.css'; + +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( + + + +); diff --git a/frontend/src/models/document.ts b/frontend/src/models/document.ts new file mode 100644 index 0000000..462bcf5 --- /dev/null +++ b/frontend/src/models/document.ts @@ -0,0 +1,99 @@ +import { API_BASE } from '../utils/constants'; +import { baseHeaders } from '../utils/request'; + +const Document = { + get: async (id: string | number) => { + return fetch(`${API_BASE}/v1/document/${id}`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.document || null) + .catch((e) => { + console.error(e); + return null; + }); + }, + source: async (id: string | number) => { + return fetch(`${API_BASE}/v1/document/${id}/source`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return null; + }); + }, + fragments: async (id: string) => { + return fetch(`${API_BASE}/v1/document/${id}/fragments`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.fragments || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + deleteFragment: async (id: string | number) => { + return fetch(`${API_BASE}/v1/document/${id}/fragment`, { + method: 'DELETE', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + updateFragment: async (id: string | number, newText: string) => { + return fetch(`${API_BASE}/v1/document/${id}/fragment`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ newText }), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + delete: async (id: string | number) => { + return fetch(`${API_BASE}/v1/document/${id}`, { + method: 'DELETE', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.ok) + .catch((e) => { + console.error(e); + return false; + }); + }, + clone: async (id: string | number, toWorkspaceId: null | string | number) => { + return fetch(`${API_BASE}/v1/document/${id}/clone`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ toWorkspaceId }), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, +}; + +export default Document; diff --git a/frontend/src/models/organization.ts b/frontend/src/models/organization.ts new file mode 100644 index 0000000..972b58d --- /dev/null +++ b/frontend/src/models/organization.ts @@ -0,0 +1,196 @@ +import { API_BASE } from '../utils/constants'; +import { baseHeaders } from '../utils/request'; + +const Organization = { + create: async (orgName: string) => { + let error; + const organization = await fetch(`${API_BASE}/v1/org/create`, { + method: 'POST', + cache: 'no-cache', + headers: baseHeaders(), + body: JSON.stringify({ orgName }), + }) + .then((res) => res.json()) + .then((res) => { + error = res?.error || '[001] Failed to create organization.'; + return res.organization; + }) + .catch((e) => { + console.error(e); + error = '[002] Failed to create organization.'; + return null; + }); + + if (!organization) return { organization: null, error }; + return { organization, error: null }; + }, + bySlug: async (slug: string) => { + const organization = await fetch(`${API_BASE}/v1/org/${slug}`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.organization) + .catch((e) => { + console.error(e); + return null; + }); + + return { organization }; + }, + all: async () => { + return await fetch(`${API_BASE}/v1/orgs/all`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.organizations || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + stats: async (slug: string, metric: string) => { + return fetch(`${API_BASE}/v1/org/${slug}/statistics/${metric}`, { + method: 'GET', + headers: baseHeaders(), + }).then((res) => res.json()); + }, + documents: async (slug: string) => { + return fetch(`${API_BASE}/v1/org/${slug}/documents`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.documents || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + workspaces: async (slug: string) => { + return fetch(`${API_BASE}/v1/org/${slug}/workspaces`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.workspaces || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + apiKey: async (slug: string) => { + return fetch(`${API_BASE}/v1/org/${slug}/api-key`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.apiKey || null) + .catch((e) => { + console.error(e); + return null; + }); + }, + connector: async (slug: string) => { + return fetch(`${API_BASE}/v1/org/${slug}/connection`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.connector || null) + .catch((e) => { + console.error(e); + return null; + }); + }, + addConnector: async ( + slug: string, + config = {} + ): Promise<{ connector: any; error: null | string }> => { + return fetch(`${API_BASE}/v1/org/${slug}/add-connection`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ config }), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { connector: null, error: e.message }; + }); + }, + updateConnector: async ( + slug: string, + config = {} + ): Promise<{ connector: any; error: null | string }> => { + return fetch(`${API_BASE}/v1/org/${slug}/update-connection`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ config }), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { connector: null, error: e.message }; + }); + }, + syncConnector: async ( + slug: string, + connectorId: number + ): Promise<{ job: any; error: null | string }> => { + return fetch(`${API_BASE}/v1/org/${slug}/connector/${connectorId}/sync`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { job: null, error: e.message }; + }); + }, + connectorCommand: async ( + slug: string, + command: string, + params: object = {} + ): Promise => { + return fetch(`${API_BASE}/v1/org/${slug}/connector/${command}`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify(params), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { result: null, error: e.message }; + }); + }, + jobs: async (slug: string) => { + return fetch(`${API_BASE}/v1/org/${slug}/jobs`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res.jobs || []) + .catch((e) => { + console.error(e); + return []; + }); + }, +}; + +export default Organization; diff --git a/frontend/src/models/system.ts b/frontend/src/models/system.ts new file mode 100644 index 0000000..417b872 --- /dev/null +++ b/frontend/src/models/system.ts @@ -0,0 +1,77 @@ +import { API_BASE } from '../utils/constants'; +import { baseHeaders } from '../utils/request'; + +const System = { + hasSetting: async ( + label: string + ): Promise<{ label: string; exists: boolean }> => { + return fetch(`${API_BASE}/system/setting/${label}/exists`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { label, exists: false, error: e.message }; + }); + }, + getSetting: async (label: string): Promise<{ label: string; value: any }> => { + return fetch(`${API_BASE}/system/setting/${label}`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { label, value: null, error: e.message }; + }); + }, + updateSettings: async ( + config = {} + ): Promise<{ success: boolean; error: null | string }> => { + return fetch(`${API_BASE}/system/update-settings`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ config }), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + documentProcessorOnline: async () => { + return fetch(`${API_BASE}/v1/document-processor/status`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.ok) + .then((res) => res) + .catch((e) => { + console.error(e); + return false; + }); + }, + acceptedDocumentTypes: async () => { + return fetch(`${API_BASE}/v1/document-processor/filetypes`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.types) + .catch((e) => { + console.error(e); + return null; + }); + }, +}; + +export default System; diff --git a/frontend/src/models/user.ts b/frontend/src/models/user.ts new file mode 100644 index 0000000..4f1bd94 --- /dev/null +++ b/frontend/src/models/user.ts @@ -0,0 +1,144 @@ +import { API_BASE } from '../utils/constants'; +import { baseHeaders } from '../utils/request'; + +const User = { + login: async (email: string, password: string) => { + let error; + const { user, valid, token } = await fetch(`${API_BASE}/auth/login`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ email, password }), + }) + .then((res) => res.json()) + .then((res) => { + error = res?.message || '[001] Failed to authenticate'; + return res; + }) + .catch((e) => { + console.error(e); + error = '[002] Failed to authenticate'; + return { user: null, token: null, valid: false }; + }); + + if (!valid) return { user: null, token: null, error }; + return { user, token, error: null }; + }, + createAccount: async (email: string, password: string) => { + let error; + const { user, valid, token } = await fetch( + `${API_BASE}/auth/create-account`, + { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ email, password }), + } + ) + .then((res) => res.json()) + .then((res) => { + error = res?.message || '[001] Failed to create account'; + return res; + }) + .catch((e) => { + console.error(e); + error = '[002] Failed to create account'; + return { user: null, token: null, valid: false }; + }); + + if (!valid) return { user: null, token: null, error }; + return { user, token, error: null }; + }, + transferRootOwnership: async (email: string, password: string) => { + let error; + const { user, valid, token } = await fetch( + `${API_BASE}/auth/transfer-root`, + { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify({ email, password }), + } + ) + .then((res) => res.json()) + .then((res) => { + error = res?.message || '[001] Failed to create account'; + return res; + }) + .catch((e) => { + console.error(e); + error = '[002] Failed to create account'; + return { user: null, token: null, valid: false }; + }); + + if (!valid) return { user: null, token: null, error }; + return { user, token, error: null }; + }, + organizations: async () => { + const organizations = await fetch(`${API_BASE}/v1/orgs`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.organizations || []) + .catch((e) => { + console.error(e); + return []; + }); + + return organizations; + }, + all: async () => { + const users = await fetch(`${API_BASE}/v1/users`, { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .then((res) => res?.users || []) + .catch((e) => { + console.error(e); + return []; + }); + + return users; + }, + delete: async (userId: string | number) => { + return await fetch(`${API_BASE}/v1/users/${userId}`, { + method: 'DELETE', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + create: async (formData: object) => { + return await fetch(`${API_BASE}/v1/user/new`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify(formData), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, + update: async (userId: string | number, formData: object) => { + return await fetch(`${API_BASE}/v1/users/${userId}`, { + method: 'POST', + cache: 'no-cache', + body: JSON.stringify(formData), + headers: baseHeaders(), + }) + .then((res) => res.json()) + .catch((e) => { + console.error(e); + return { success: false, error: e.message }; + }); + }, +}; + +export default User; diff --git a/frontend/src/models/workspace.ts b/frontend/src/models/workspace.ts new file mode 100644 index 0000000..717bdd8 --- /dev/null +++ b/frontend/src/models/workspace.ts @@ -0,0 +1,148 @@ +import { API_BASE } from '../utils/constants'; +import { baseHeaders } from '../utils/request'; + +const Workspace = { + createNew: async (orgSlug: string, workspaceName: string) => { + let error; + const workspace = await fetch( + `${API_BASE}/v1/org/${orgSlug}/new-workspace`, + { + method: 'POST', + cache: 'no-cache', + headers: baseHeaders(), + body: JSON.stringify({ workspaceName }), + } + ) + .then((res) => res.json()) + .then((res) => { + error = res?.error || '[001] Failed to create workspace.'; + return res.workspace; + }) + .catch((e) => { + console.error(e); + error = '[002] Failed to create workspace.'; + return null; + }); + + if (!workspace) return { organization: null, error }; + return { workspace, error: null }; + }, + bySlug: async (slug: string, workspaceSlug: string) => { + const workspace = await fetch( + `${API_BASE}/v1/org/${slug}/workspace/${workspaceSlug}`, + { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .then((res) => res?.workspace) + .catch((e) => { + console.error(e); + return null; + }); + + return workspace; + }, + stats: async (orgSlug: string, slug: string, metric: string) => { + return fetch( + `${API_BASE}/v1/org/${orgSlug}/workspace/${slug}/statistics/${metric}`, + { + method: 'GET', + headers: baseHeaders(), + } + ).then((res) => res.json()); + }, + documents: async (orgSlug: string, workspaceSlug: string) => { + return fetch( + `${API_BASE}/v1/org/${orgSlug}/workspace/${workspaceSlug}/documents`, + { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .then((res) => res?.documents || []) + .catch((e) => { + console.error(e); + return []; + }); + }, + delete: async (orgSlug: string, workspaceSlug: string) => { + return fetch(`${API_BASE}/v1/org/${orgSlug}/workspace/${workspaceSlug}`, { + method: 'DELETE', + cache: 'no-cache', + headers: baseHeaders(), + }) + .then((res) => res.ok) + .catch((e) => { + console.error(e); + return false; + }); + }, + uploadFile: async function ( + orgSlug: string, + workspaceSlug: string, + formData: FormData + ) { + const response = await fetch( + `${API_BASE}/v1/org/${orgSlug}/workspace/${workspaceSlug}/upload`, + { + method: 'POST', + body: formData, + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e.message); + return { success: false, error: e.message }; + }); + return response; + }, + syncConnector: async ( + orgSlug: string, + workspaceSlug: string, + connectorId: number + ): Promise<{ job: any; error: null | string }> => { + return fetch( + `${API_BASE}/v1/org/${orgSlug}/connector/${connectorId}/sync/${workspaceSlug}`, + { + method: 'GET', + cache: 'no-cache', + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e); + return { job: null, error: e.message }; + }); + }, + clone: async ( + orgSlug: string, + workspaceSlug: string, + newWorkspaceName: string | null = null + ): Promise<{ success: boolean; error: null | string }> => { + return await fetch( + `${API_BASE}/v1/org/${orgSlug}/workspace/${workspaceSlug}/clone`, + { + method: 'POST', + body: JSON.stringify({ newWorkspaceName }), + headers: baseHeaders(), + } + ) + .then((res) => res.json()) + .then((res) => res) + .catch((e) => { + console.error(e.message); + return { success: false, error: e.message }; + }); + }, +}; + +export default Workspace; diff --git a/frontend/src/pages/Authentication/SignIn.tsx b/frontend/src/pages/Authentication/SignIn.tsx new file mode 100644 index 0000000..370ca21 --- /dev/null +++ b/frontend/src/pages/Authentication/SignIn.tsx @@ -0,0 +1,241 @@ +import { Link } from 'react-router-dom'; +import LogoDark from '../../images/logo/logo-dark.png'; +import Logo from '../../images/logo/logo.png'; +import DefaultLayout from '../../layout/DefaultLayout'; +import ManageSvg from '../../images/undraws/manage.svg'; +import PreLoader from '../../components/Preloader'; +import { useEffect, useState } from 'react'; +import { CheckCircle, Key, Mail, XCircle } from 'react-feather'; +import User from '../../models/user'; +import { APP_NAME, STORE_TOKEN, STORE_USER } from '../../utils/constants'; +import paths from '../../utils/paths'; +import validateSessionTokenForUser from '../../utils/session'; + +type IStages = 'loading' | 'failed' | 'success' | 'ready'; +type FormTypes = { + target: { + email: { + value: string; + }; + password: { + value: string; + }; + }; +}; + +type IResult = { + user: any; + token: string | null; + error?: string | null; +}; + +const SignIn = () => { + const [stage, setStage] = useState('ready'); + const [results, setResults] = useState({ + user: null, + token: null, + error: null, + }); + const resetStage = () => { + setResults({ user: null, token: null, error: null }); + setStage('ready'); + }; + const handleSubmit = async (e: React.FormEvent & FormTypes) => { + e.preventDefault(); + setStage('loading'); + const { user, token, error } = await User.login( + e.target.email.value, + e.target.password.value + ); + if (!token) setStage('failed'); + if (!!token) setStage('success'); + setResults({ user, token, error }); + + if (!!token) { + window.localStorage.setItem(STORE_USER, JSON.stringify(user)); + window.localStorage.setItem(STORE_TOKEN, token); + window.location.replace( + user?.role === 'root' ? paths.systemSetup() : paths.dashboard() + ); + } + }; + + useEffect(() => { + async function checkAuth() { + const currentToken = window.localStorage.getItem(STORE_TOKEN); + if (!currentToken) return false; + const success = await validateSessionTokenForUser(); + if (!success) { + window.localStorage.removeItem(STORE_USER); + window.localStorage.removeItem(STORE_TOKEN); + return false; + } + window.location.replace(paths.dashboard()); + } + checkAuth(); + }); + + return ( + +
    +
    +
    +
    + + Logo + Logo + + +

    + Did you know using {APP_NAME} can save you 75% on embedding + costs? +

    + + + + +
    +
    + +
    +
    + {stage !== 'ready' ? ( + + ) : ( + + )} +
    +
    +
    +
    +
    + ); +}; + +function ShowStatus({ + stage, + results, + resetForm, +}: { + stage: IStages; + results: IResult; + resetForm: any; +}) { + if (stage === 'loading') { + return ( +
    + +

    logging you in...

    +
    + ); + } + + if (stage === 'failed') { + return ( +
    + +

    + We could not log you in - contact an admin. +

    +

    {results?.error}

    + +
    + ); + } + + if (stage === 'success') { + return ( +
    + +

    Login was successful!

    +

    + Redirecting you to the right place. +

    +
    + ); + } + + return null; +} + +function LoginForm({ handleSubmit }: { handleSubmit: any }) { + return ( + <> + Sign back in +
    +
    + +
    + + + + + +
    +
    + +
    + +
    + + + + + +
    +
    + +
    + +
    + +
    +

    + Don't have a {APP_NAME} account?{' '} + + Sign Up + +

    +
    +
    + + ); +} + +export default SignIn; diff --git a/frontend/src/pages/Authentication/SignUp.tsx b/frontend/src/pages/Authentication/SignUp.tsx new file mode 100644 index 0000000..51416b6 --- /dev/null +++ b/frontend/src/pages/Authentication/SignUp.tsx @@ -0,0 +1,223 @@ +import { Link } from 'react-router-dom'; +import LogoDark from '../../images/logo/logo-dark.png'; +import Logo from '../../images/logo/logo.png'; +import ManageSvg from '../../images/undraws/manage.svg'; +import DefaultLayout from '../../layout/DefaultLayout'; +import { useState } from 'react'; +import PreLoader from '../../components/Preloader'; +import { CheckCircle, Key, Mail, XCircle } from 'react-feather'; +import User from '../../models/user'; +import { APP_NAME, STORE_TOKEN, STORE_USER } from '../../utils/constants'; +import paths from '../../utils/paths'; + +type IStages = 'loading' | 'failed' | 'success' | 'ready'; +type FormTypes = { + target: { + email: { + value: string; + }; + password: { + value: string; + }; + }; +}; + +type IResult = { + user: any; + token: string | null; + error?: string | null; +}; + +const SignUp = () => { + const [stage, setStage] = useState('ready'); + const [results, setResults] = useState({ + user: null, + token: null, + error: null, + }); + const resetStage = () => { + setResults({ user: null, token: null, error: null }); + setStage('ready'); + }; + const handleSubmit = async (e: React.FormEvent & FormTypes) => { + e.preventDefault(); + setStage('loading'); + const { user, token, error } = await User.createAccount( + e.target.email.value, + e.target.password.value + ); + if (!token) setStage('failed'); + if (!!token) setStage('success'); + setResults({ user, token, error }); + + if (!!token) { + window.localStorage.setItem(STORE_USER, JSON.stringify(user)); + window.localStorage.setItem(STORE_TOKEN, token); + window.location.replace(paths.dashboard()); + } + }; + + return ( + +
    +
    +
    +
    + + Logo + Logo + +

    + You are almost ready to use the most powerful vector data + management system ever. +

    + + + +
    +
    + +
    +
    + {stage !== 'ready' ? ( + + ) : ( + + )} +
    +
    +
    +
    +
    + ); +}; + +function ShowStatus({ + stage, + results, + resetForm, +}: { + stage: IStages; + results: IResult; + resetForm: any; +}) { + if (stage === 'loading') { + return ( +
    + +

    creating account...

    +
    + ); + } + + if (stage === 'failed') { + return ( +
    + +

    We could not create an account.

    +

    {results?.error}

    + +
    + ); + } + + if (stage === 'success') { + return ( +
    + +

    Your account was created!

    +

    + Redirecting you to your dashboard +

    +
    + ); + } + + return null; +} + +function LoginForm({ handleSubmit }: { handleSubmit: any }) { + return ( + <> + New account +

    + Sign Up for {APP_NAME} +

    + +
    +
    + +
    + + + + + +
    +
    + +
    + +
    + + + + + +
    +
    + +
    + +
    + +
    +

    + Already have an account?{' '} + + Sign in + +

    +
    +
    + + ); +} + +export default SignUp; diff --git a/frontend/src/pages/Authentication/SystemSetup.tsx b/frontend/src/pages/Authentication/SystemSetup.tsx new file mode 100644 index 0000000..0915602 --- /dev/null +++ b/frontend/src/pages/Authentication/SystemSetup.tsx @@ -0,0 +1,228 @@ +import { Link } from 'react-router-dom'; +import LogoDark from '../../images/logo/logo-dark.png'; +import Logo from '../../images/logo/logo.png'; +import DefaultLayout from '../../layout/DefaultLayout'; +import ManageSvg from '../../images/undraws/manage.svg'; +import PreLoader from '../../components/Preloader'; +import { useState } from 'react'; +import { CheckCircle, Key, Mail, XCircle } from 'react-feather'; +import User from '../../models/user'; +import { APP_NAME, STORE_TOKEN, STORE_USER } from '../../utils/constants'; +import paths from '../../utils/paths'; + +type IStages = 'loading' | 'failed' | 'success' | 'ready'; +type FormTypes = { + target: { + email: { + value: string; + }; + password: { + value: string; + }; + }; +}; + +type IResult = { + user: any; + token: string | null; + error?: string | null; +}; + +const SystemSetup = () => { + const [stage, setStage] = useState('ready'); + const [results, setResults] = useState({ + user: null, + token: null, + error: null, + }); + const resetStage = () => { + setResults({ user: null, token: null, error: null }); + setStage('ready'); + }; + const handleSubmit = async (e: React.FormEvent & FormTypes) => { + e.preventDefault(); + setStage('loading'); + const { user, token, error } = await User.transferRootOwnership( + e.target.email.value, + e.target.password.value + ); + if (!token) setStage('failed'); + if (!!token) setStage('success'); + setResults({ user, token, error }); + + if (!!token) { + window.localStorage.setItem(STORE_USER, JSON.stringify(user)); + window.localStorage.setItem(STORE_TOKEN, token); + window.location.replace(paths.onboarding.orgName()); + } + }; + + return ( + +
    +
    +
    +
    + + Logo + Logo + + +

    + Did you know using {APP_NAME} can save you 75% on embedding + costs? +

    + + + + +
    +
    + +
    +
    + {stage !== 'ready' ? ( + + ) : ( + + )} +
    +
    +
    +
    +
    + ); +}; + +function ShowStatus({ + stage, + results, + resetForm, +}: { + stage: IStages; + results: IResult; + resetForm: any; +}) { + if (stage === 'loading') { + return ( +
    + +

    making you the system admin...

    +
    + ); + } + + if (stage === 'failed') { + return ( +
    + +

    + We could not complete this process - check the system logs. +

    +

    {results?.error}

    + +
    + ); + } + + if (stage === 'success') { + return ( +
    + +

    + Root account was deleted and you are now the system admin! +

    +

    + Redirecting you to the right place! +

    +
    + ); + } + + return null; +} + +function LoginForm({ handleSubmit }: { handleSubmit: any }) { + return ( + <> + + Create the System Administrator + +

    + By default {APP_NAME} creates a temporary root account so you can set up + a system admin account. After creation of this account the root account + will no longer be accessible and you will use these credentials to login + going forward. +
    +
    + If you lose your password you will never be able to recover it - so keep + it safe. +

    +
    +
    + +
    + + + + + +
    +
    + +
    + +
    + + + + + +
    +
    + +
    + +
    +
    + + ); +} + +export default SystemSetup; diff --git a/frontend/src/pages/Dashboard/ApiKey/index.tsx b/frontend/src/pages/Dashboard/ApiKey/index.tsx new file mode 100644 index 0000000..2e52937 --- /dev/null +++ b/frontend/src/pages/Dashboard/ApiKey/index.tsx @@ -0,0 +1,100 @@ +import { useEffect, useState } from 'react'; +import { Copy } from 'react-feather'; +import PreLoader from '../../../components/Preloader'; +import Organization from '../../../models/organization'; +import { APP_NAME } from '../../../utils/constants'; + +export default function ApiKeyCard({ organization }: { organization?: any }) { + const [loading, setLoading] = useState(true); + const [apiKey, setApiKey] = useState(null); + + useEffect(() => { + async function fetchApi() { + if (!organization?.slug) return; + const orgApiKey = await Organization.apiKey(organization.slug); + setApiKey(orgApiKey.apiKey); + setLoading(false); + } + fetchApi(); + }, [organization?.slug]); + + return ( +
    +
    +

    Copy API Key

    +

    + For use of syncing {APP_NAME} with your workflows programmatically. +

    +
    + +
    + {loading ? : } +
    +
    + ); +} + +const ApiKey = ({ secret }: { secret: string | null }) => { + const [show, setShow] = useState(false); + const copyCode = () => { + if (!secret) return; + setShow(true); + window.navigator.clipboard.writeText(secret); + return; + }; + + const displaySecret = () => { + if (!secret) return ''; + if (show) { + return secret; + } + + const sections = secret.split('vdms-')?.[1]?.split('-'); + return ( + 'vdms-' + + sections + .map((section, i) => { + if (i < sections.length) + return section + .split('') + .map(() => '*') + .join(''); + return section; + }) + .join('-') + ); + }; + + useEffect(() => { + if (!show) return; + setTimeout(() => { + setShow(false); + }, 3500); + }, [show]); + + return ( +
  • +
    +
    +

    + {displaySecret()} +

    +
    +
    + +
    +
    +
  • + ); +}; diff --git a/frontend/src/pages/Dashboard/Connector/index.tsx b/frontend/src/pages/Dashboard/Connector/index.tsx new file mode 100644 index 0000000..931fea2 --- /dev/null +++ b/frontend/src/pages/Dashboard/Connector/index.tsx @@ -0,0 +1,786 @@ +import { useEffect, useState } from 'react'; +import { CheckCircle, Circle, XCircle } from 'react-feather'; +import PreLoader from '../../../components/Preloader'; +import Organization from '../../../models/organization'; +import { APP_NAME } from '../../../utils/constants'; +import ChromaLogo from '../../../images/vectordbs/chroma.png'; +import PineconeLogo from '../../../images/vectordbs/pinecone.png'; +import paths from '../../../utils/paths'; +import { titleCase } from 'title-case'; + +export default function ConnectorCard({ + knownConnector, + organization, + workspaces, +}: { + knownConnector?: any; + organization?: any; + workspaces?: any[]; +}) { + const [loading, setLoading] = useState(true); + const [connector, setConnector] = useState(null); + const [canSync, setCanSync] = useState(false); + + useEffect(() => { + async function fetchConnector() { + if (!!knownConnector) { + if ( + knownConnector.type === 'chroma' || + knownConnector.type === 'pinecone' + ) { + const { result } = await Organization.connectorCommand( + organization.slug, + 'totalIndicies' + ); + if (!!result && result > 0 && workspaces?.length === 0) + setCanSync(true); + } + + setLoading(false); + setConnector(knownConnector); + return; + } + + setLoading(false); + } + fetchConnector(); + }, []); + + if (loading) { + return ( +
    +
    +

    + Vector Database Connection +

    + +
    + +
    +
    + ); + } + + if (!connector) { + return ( + <> +
    +
    +

    + Vector Database Connection +

    + +
    + +
    +

    + You have no vector database connected to this organization. You + will be limited to read-only permissions until full setup. +

    + +
    +
    + + + ); + } + + return ( + <> +
    +
    +

    + Vector Database Connection +

    + +
    + +
    +

    + You are currently connected to a {connector.type} instance. +

    +
    + {canSync ? ( + <> + + + + ) : ( + + )} +
    +
    +
    + + + + ); +} + +const NewConnectorModal = ({ + organization, + onNew, +}: { + organization: any; + onNew: (newConnector: any) => void; +}) => { + const [loading, setLoading] = useState(false); + const [type, setType] = useState('chroma'); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + + const handleSubmit = async (e: any) => { + e.preventDefault(); + setError(null); + setLoading(true); + const data = { type }; + const form = new FormData(e.target); + for (var [_k, value] of form.entries()) { + if (_k.includes('::')) { + const [_key, __key] = _k.split('::'); + if (!data.hasOwnProperty(_key)) data[_key] = {}; + data[_key][__key] = value; + } else { + data[_k] = value; + } + } + + const { connector, error } = await Organization.addConnector( + organization.slug, + data + ); + if (!connector) { + setLoading(false); + setError(error); + return false; + } + + setLoading(false); + setSuccess(true); + setTimeout(() => { + onNew(connector); + setSuccess(false); + }, 1500); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
    +
    +

    + Connect to Vector Database +

    +

    + {APP_NAME} is a tool to help you manage vectors in a vector + database, but without access to a valid vector database you will be + limited to read-only actions and limited functionality - you should + connect to a vector database provider to unlock full functionality. +

    +
    + {loading ? ( +
    +
    + +
    +
    + ) : ( +
    +
      +
    • setType('chroma')}> + + +
    • +
    • setType('pinecone')}> + + +
    • +
    + + {type === 'chroma' && ( +
    +
    +
    + +

    + This is the URL your chroma instance is reachable at. +

    +
    + +
    +
    +
    + +

    + If your hosted Chroma instance is protected by an API key + - enter it here. +

    +
    + +
    +
    + )} + + {type === 'pinecone' && ( +
    +
    +
    + +

    + You can find this on your Pinecone index. +

    +
    + +
    + +
    +
    + +

    + You can find this on your Pinecone index. +

    +
    + +
    + +
    +
    + +

    + If your hosted Chroma instance is protected by an API key + - enter it here. +

    +
    + +
    +
    + )} + +
    + {error && ( +

    + {error} +

    + )} + {success && ( +

    + Connector added to organization +

    + )} + +
    +
    + )} +
    +
    + ); +}; + +const UpdateConnectorModal = ({ + organization, + connector, + onUpdate, +}: { + organization: any; + connector: any; + onUpdate: (newConnector: any) => void; +}) => { + const [loading, setLoading] = useState(false); + const [type, setType] = useState(connector.type); + const [error, setError] = useState(null); + const [success, setSuccess] = useState(false); + const settings = JSON.parse(connector.settings); + + const handleSubmit = async (e: any) => { + e.preventDefault(); + setError(null); + setLoading(true); + const data = { type }; + const form = new FormData(e.target); + for (var [_k, value] of form.entries()) { + if (_k.includes('::')) { + const [_key, __key] = _k.split('::'); + if (!data.hasOwnProperty(_key)) data[_key] = {}; + data[_key][__key] = value; + } else { + data[_k] = value; + } + } + + const { connector, error } = await Organization.updateConnector( + organization.slug, + data + ); + if (!connector) { + setLoading(false); + setError(error); + return false; + } + + setLoading(false); + setSuccess(true); + setTimeout(() => { + onUpdate(connector); + setSuccess(false); + }, 1500); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
    +
    +

    + Update Vector Database Connection +

    +

    + {APP_NAME} is currently connected to a {connector.type} vector + database instance. You can update your configuration settings here + if they have changed. +

    +
    + {loading ? ( +
    +
    + +
    +
    + ) : ( +
    +
      +
    • setType('chroma')}> + + +
    • +
    • setType('pinecone')}> + + +
    • +
    + + {type === 'chroma' && ( +
    +
    +
    + +

    + This is the URL your chroma instance is reachable at. +

    +
    + +
    +
    +
    + +

    + If your hosted Chroma instance is protected by an API key + - enter it here. +

    +
    + +
    +
    + )} + + {type === 'pinecone' && ( +
    +
    +
    + +

    + You can find this on your Pinecone index. +

    +
    + +
    + +
    +
    + +

    + You can find this on your Pinecone index. +

    +
    + +
    + +
    +
    + +

    + If your hosted Chroma instance is protected by an API key + - enter it here. +

    +
    + +
    +
    + )} + +
    + {error && ( +

    + {error} +

    + )} + {success && ( +

    + Connector changes saved +

    + )} + +
    +
    + )} +
    +
    + ); +}; + +const SyncConnectorModal = ({ + organization, + connector, +}: { + organization: any; + connector: any; +}) => { + const [synced, setSynced] = useState(false); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const sync = async () => { + setError(null); + setLoading(true); + const { job, error } = await Organization.syncConnector( + organization.slug, + connector.id + ); + + if (!job) { + setError(error); + setLoading(false); + setSynced(false); + return; + } + + setLoading(false); + setSynced(true); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
    +
    +

    + Sync Vector Database Connection +

    +

    + {APP_NAME} can automatically sync existing information in your{' '} + {titleCase(connector.type)}{' '} + {connector.type === 'chroma' ? 'collections' : 'namespaces'} so you + can manage it more easily. This process can take a long time to + complete depending on how much data you have embedded already. +
    +
    + Once you start this process you can check on its progress in the{' '} + + job queue. + +

    +
    +
    + {error && ( +

    + {error} +

    + )} + {synced ? ( + + ) : ( + + )} +
    +
    +
    + ); +}; diff --git a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx new file mode 100644 index 0000000..6af6c7e --- /dev/null +++ b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/FileUploadProgress/index.tsx @@ -0,0 +1,84 @@ +import { useState, useEffect, memo } from 'react'; +import Workspace from '../../../../../models/workspace'; +import truncate from 'truncate'; +import { humanFileSize, milliToHms } from '../../../../../utils/numbers'; +import { CheckCircle, XCircle } from 'react-feather'; +import { Grid } from 'react-loading-icons'; + +function FileUploadProgressComponent({ + slug, + workspace, + file, + rejected = false, + reason = null, +}: { + workspace: any; + slug: string; + file: any; + rejected: any; + reason: any; +}) { + const [timerMs, setTimerMs] = useState(10); + const [status, setStatus] = useState(file?.rejected ? 'uploading' : 'failed'); + + useEffect(() => { + async function uploadFile() { + const start = Number(new Date()); + const formData = new FormData(); + formData.append('file', file, file.name); + const timer = setInterval(() => { + setTimerMs(Number(new Date()) - start); + }, 100); + + // Chunk streaming not working in production so we just sit and wait + const { success } = await Workspace.uploadFile( + slug, + workspace.slug, + formData + ); + setStatus(success ? 'complete' : 'failed'); + clearInterval(timer); + } + !!file && !rejected && uploadFile(); + }, []); + + if (rejected) { + return ( +
    +
    + +
    +
    +

    + {truncate(file.name, 30)} +

    +

    + {reason} +

    +
    +
    + ); + } + + return ( +
    +
    + {status !== 'complete' ? ( + + ) : ( + + )} +
    +
    +

    + {truncate(file.name, 30)} +

    +

    + {humanFileSize(file.size)} | {milliToHms(timerMs)} +

    +
    +
    + ); +} + +export default memo(FileUploadProgressComponent); diff --git a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/UploadModalNoKey.tsx b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/UploadModalNoKey.tsx new file mode 100644 index 0000000..0b1f317 --- /dev/null +++ b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/UploadModalNoKey.tsx @@ -0,0 +1,68 @@ +import { AlertTriangle } from 'react-feather'; +import { APP_NAME } from '../../../../utils/constants'; +import System from '../../../../models/system'; + +export default function UploadModalNoKey() { + const updateSystemSetting = async (e: any) => { + e.preventDefault(); + const form = new FormData(e.target); + const open_ai_api_key = form.get('open_ai_api_key'); + await System.updateSettings({ open_ai_api_key }); + window.location.reload(); + }; + + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
    +

    + Upload new document +

    +
    +
    +
    +
    + You cannot upload and embed documents without an + OpenAI API Key. +
    +

    + {APP_NAME} will automatically upload and embed your documents for + you, but for this to happen we must have an OpenAI key set. +

    +
    +
    +
    + + +
    +
    + +
    +
    +

    + This key will only be used for the embedding of documnets you + upload via {APP_NAME}. +

    +
    +
    +
    +
    + ); +} diff --git a/frontend/src/pages/Dashboard/DocumentsList/UploadModal/index.tsx b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/index.tsx new file mode 100644 index 0000000..aa37d1d --- /dev/null +++ b/frontend/src/pages/Dashboard/DocumentsList/UploadModal/index.tsx @@ -0,0 +1,213 @@ +import { useCallback, useState, useEffect, ReactNode } from 'react'; +import { APP_NAME } from '../../../../utils/constants'; +import { useDropzone } from 'react-dropzone'; +import { v4 } from 'uuid'; +import System from '../../../../models/system'; +import { Frown } from 'react-feather'; +import FileUploadProgress from './FileUploadProgress'; +import { useParams } from 'react-router-dom'; + +export default function UploadDocumentModal({ + workspaces, +}: { + workspaces: any; +}) { + const { slug } = useParams(); + const [targetWorkspace, setTargetWorkspace] = useState(null); + const [ready, setReady] = useState(null); + const [files, setFiles] = useState([]); + const [fileTypes, setFileTypes] = useState({}); + const onDrop = useCallback( + async (acceptedFiles: any[], rejections: any[]) => { + const newAccepted = acceptedFiles.map((file) => { + return { + uid: v4(), + workspaceId: targetWorkspace?.id, + file, + }; + }); + const newRejected = rejections.map((file) => { + return { + uid: v4(), + file: file.file, + rejected: true, + reason: file.errors[0].code, + }; + }); + + setFiles([...files, ...newAccepted, ...newRejected]); + }, + [] + ); + + useEffect(() => { + async function checkProcessorOnline() { + const online = await System.documentProcessorOnline(); + const acceptedTypes = await System.acceptedDocumentTypes(); + setFileTypes(acceptedTypes ?? {}); + setReady(online); + } + checkProcessorOnline(); + }, []); + + const { getRootProps, getInputProps } = useDropzone({ + onDrop, + accept: { + ...fileTypes, + }, + }); + + if (ready === null || !slug) { + return ( + +
    +
    +

    + Checking document processor is online - please wait. +

    +

    + this should only take a few moments. +

    +
    +
    +
    + ); + } + + if (ready === false) { + return ( + +
    +
    + +

    + Document processor is offline. +

    +

    + you cannot upload documents from the UI right now +

    +
    +
    +
    + ); + } + + if (ready === true && targetWorkspace === null) { + const saveWorkspace = (e: any) => { + e.preventDefault(); + const form = new FormData(e.target); + const selectedWsId = form.get('workspaceId'); + const workspace = workspaces.find( + (ws: any) => ws.id === Number(selectedWsId) + ); + if (!workspace) return false; + setTargetWorkspace(workspace); + }; + + return ( + +
    +
    +

    + Please select the workspace you wish to upload documents to. +

    +
    + + +
    +
    +
    +
    + ); + } + + return ( + +
    +
    + + {files.length === 0 ? ( +
    +
    + +

    + Click to upload or drag + and drop +

    +

    +
    +
    + ) : ( +
    + {files.map((file) => ( + + ))} +
    + )} +
    +

    + supported file extensions are{' '} + + {Object.values(fileTypes).flat().join(' ')} + +

    +
    +
    + ); +} + +const ModalWrapper = ({ children }: { children: ReactNode }) => { + return ( + { + event.target == event.currentTarget && event.currentTarget?.close(); + }} + > +
    +

    + Upload new document +

    +

    + Select a workspace and document you wish to upload and {APP_NAME} will + process, embed and store the data for you automatically. +

    +
    +
    {children}
    +
    + ); +}; diff --git a/frontend/src/pages/Dashboard/DocumentsList/index.tsx b/frontend/src/pages/Dashboard/DocumentsList/index.tsx new file mode 100644 index 0000000..aa51eb5 --- /dev/null +++ b/frontend/src/pages/Dashboard/DocumentsList/index.tsx @@ -0,0 +1,316 @@ +import { Link } from 'react-router-dom'; +import paths from '../../../utils/paths'; +import moment from 'moment'; +import { AlertOctagon, FileText } from 'react-feather'; +// import { CodeBlock, vs2015 } from 'react-code-blocks'; +import { useEffect, useState } from 'react'; +import Organization from '../../../models/organization'; +import truncate from 'truncate'; +import System from '../../../models/system'; +import UploadDocumentModal from './UploadModal'; +import UploadModalNoKey from './UploadModal/UploadModalNoKey'; + +export default function DocumentsList({ + organization, + workspaces, + knownConnector, +}: { + organization: any; + workspaces: any; + knownConnector: any; +}) { + const [loading, setLoading] = useState(true); + const [documents, setDocuments] = useState([]); + const [canUpload, setCanUpload] = useState(false); + + useEffect(() => { + async function getDocs(slug?: string) { + if (!slug) return false; + const documents = await Organization.documents(slug); + const { exists: hasOpenAIKey } = await System.hasSetting( + 'open_ai_api_key' + ); + setDocuments(documents); + setCanUpload(hasOpenAIKey); + setLoading(false); + } + getDocs(organization.slug); + }, [organization.slug]); + + if (loading) { + return ( +
    +
    +
    +

    + Documents {documents.length > 0 ? `(${documents.length})` : ''} +

    +
    +
    +
    +
    +
    +
    + ); + } + + return ( + <> +
    +
    +
    +

    + Documents {documents.length > 0 ? `(${documents.length})` : ''} +

    +
    + {workspaces.length > 0 ? ( + <> + {!!knownConnector ? ( + + ) : ( + + )} + + ) : ( + + )} +
    + {documents.length > 0 ? ( +
    +
    +
    +
    + Document Name +
    +
    + Workspace +
    +
    + Created +
    +
    + Status +
    +
    + +
    +
    +
    + <> + {documents.map((document) => { + return ( +
    +
    +
    +
    + + + {truncate(document.name, 20)} + +
    +
    +
    + + + {document.workspace.name || ''} + + +
    +
    + + {moment.unix(document.createdAt).format('lll')} + +
    +
    + + Cached + +
    +
    + + Details + +
    +
    +
    + ); + })} + +
    + ) : ( + <> + {workspaces.length === 0 ? ( +
    +
    +
    +

    You have no documents or workspaces available!

    +

    + Get started by creating a workspace, then you can start + adding documents via the UI or code. +

    +
    +
    +
    + ) : ( + <> +
    +
    +
    +

    You have no documents in any workspaces!

    +

    + Get started managing documents by adding them to + workspaces via the UI or code. +

    + +
    +
    +
    + {/* */} + + )} + + )} +
    + {canUpload ? ( + + ) : ( + + )} + + ); +} + +// const CodeExampleModal = ({ organization }: { organization: any }) => { +// // Rework this to be an upload modal. +// return ( +// +//
    +//
    +//

    +// Adding documents to Conifer +//

    +//

    +// You can begin managing documents with the code you have already +// written. Our library currently only supports NodeJS environments. +//

    +//
    + +//

    +// During the Pinecone Hackathon the library is a standalone fork of +// langchainJS, but ideally it would eventually be live in the main +// LangchainJS repo :) +//
    +// We werent able to add uploading or deleting docs via the UI but how +// cool would that be. It can be done via the library though. +//

    + +//
    +// +//
    + +//
    +// +//
    +//
    +//
    +// ); +// }; diff --git a/frontend/src/pages/Dashboard/Statistics/index.tsx b/frontend/src/pages/Dashboard/Statistics/index.tsx new file mode 100644 index 0000000..4d26fbc --- /dev/null +++ b/frontend/src/pages/Dashboard/Statistics/index.tsx @@ -0,0 +1,100 @@ +import { memo, useState, useEffect } from 'react'; +import PreLoader from '../../../components/Preloader'; +import { humanFileSize, nFormatter } from '../../../utils/numbers'; +import moment from 'moment'; +import Organization from '../../../models/organization'; +import pluralize from 'pluralize'; + +const Statistics = ({ organization }: { organization: any }) => { + const [documents, setDocuments] = useState({ + status: 'loading', + value: 0, + }); + const [vectors, setVectors] = useState({ + status: 'loading', + value: 0, + }); + const [cache, setCache] = useState({ + status: 'loading', + value: 0, + }); + + useEffect(() => { + async function collectStats() { + if (!organization?.slug) return false; + + Organization.stats(organization.slug, 'documents').then((json) => { + setDocuments({ status: 'complete', value: json.value }); + }); + Organization.stats(organization.slug, 'vectors').then((json) => { + setVectors({ status: 'complete', value: json.value }); + }); + Organization.stats(organization.slug, 'cache-size').then((json) => { + setCache({ status: 'complete', value: json.value }); + }); + } + collectStats(); + }, [organization?.slug]); + + return ( +
    +
    +
    + {documents.status === 'loading' ? ( + + ) : ( +
    +

    + {nFormatter(documents.value)} +

    +

    + {pluralize('Document', documents.value)} +

    +
    + )} +
    + +
    + {vectors.status === 'loading' ? ( + + ) : ( +
    +

    + {nFormatter(vectors.value)} +

    +

    + {pluralize('Vector', vectors.value)} +

    +
    + )} +
    + +
    + {cache.status === 'loading' ? ( + + ) : ( +
    +

    + {humanFileSize(cache.value)} +

    +

    Vector Cache (MB)

    +
    + )} +
    + +
    +
    +

    + {organization?.lastUpdated + ? moment.unix(organization.lastUpdated).fromNow() + : moment.unix(organization.createdAt).fromNow()} +

    +

    Last Modified

    +
    +
    +
    +
    + ); +}; + +export default memo(Statistics); diff --git a/frontend/src/pages/Dashboard/WorkspacesList/index.tsx b/frontend/src/pages/Dashboard/WorkspacesList/index.tsx new file mode 100644 index 0000000..657573f --- /dev/null +++ b/frontend/src/pages/Dashboard/WorkspacesList/index.tsx @@ -0,0 +1,208 @@ +import { Link } from 'react-router-dom'; +import Jazzicon from '../../../components/Jazzicon'; +import paths from '../../../utils/paths'; +import moment from 'moment'; +import { useState } from 'react'; +import Workspace from '../../../models/workspace'; +import PreLoader from '../../../components/Preloader'; +import { nFormatter } from '../../../utils/numbers'; +import { File } from 'react-feather'; +import truncate from 'truncate'; + +export default function WorkspacesList({ + knownConnector, + organization, + workspaces, +}: { + knownConnector: any; + organization: any; + workspaces: any[]; +}) { + return ( +
    +
    +

    + Workspaces {workspaces.length > 0 ? `(${workspaces.length})` : ''} +

    + {workspaces.length > 0 && ( + + )} +
    + + {workspaces.length > 0 ? ( +
    + <> + {workspaces.map((workspace) => { + return ( + +
    + +
    + +
    +
    +
    + {workspace.name} +
    +

    + + {truncate(workspace.workspaceId, 15)} + +

    +
    +
    +
    +

    + {nFormatter(workspace?.documentCount || 0)} +

    + +
    +

    + + created {moment.unix(workspace.createdAt).fromNow()} + +

    +
    +
    + + ); + })} + +
    + ) : ( + <> + {!!knownConnector ? ( +
    +
    +
    + +
    +
    +
    + ) : ( +
    +
    +
    +

    + Creation of workspaces is disabled until you add a vector + database connection +

    +
    +
    +
    + )} + + )} + + +
    + ); +} + +const CreateWorkspaceModal = ({ organization }: { organization: any }) => { + const [loading, setLoading] = useState(false); + const handleSubmit = async (e: any) => { + e.preventDefault(); + setLoading(true); + const { workspace } = await Workspace.createNew( + organization.slug, + e.target.name.value + ); + if (!workspace) { + setLoading(false); + return false; + } + + window.location.reload(); + }; + + return ( + +
    +
    +

    + Create a New Workspace +

    +

    + Workspaces are collections of documents inside of your organization. + They allow you to control permissions and documents with ultimate + visibility. +
    + + They should match with what you are calling your namespaces or + collections in your vector database. + +

    +
    + {loading ? ( +
    +
    + +
    +
    + ) : ( +
    +
    +
    + + +
    +
    + + +
    +

    + Once your workspace exists you can start adding documents via + the UI or API via code. +

    +
    +
    + )} +
    +
    + ); +}; diff --git a/frontend/src/pages/Dashboard/index.tsx b/frontend/src/pages/Dashboard/index.tsx new file mode 100644 index 0000000..af86509 --- /dev/null +++ b/frontend/src/pages/Dashboard/index.tsx @@ -0,0 +1,95 @@ +import { FullScreenLoader } from '../../components/Preloader'; +import useUser from '../../hooks/useUser'; +import { useState, useEffect } from 'react'; +import DefaultLayout from '../../layout/DefaultLayout'; +import User from '../../models/user'; +import paths from '../../utils/paths'; +import AppLayout from '../../layout/AppLayout'; +import { useParams } from 'react-router-dom'; +import Statistics from './Statistics'; +import WorkspacesList from './WorkspacesList'; +import DocumentsList from './DocumentsList'; +import Organization from '../../models/organization'; +import ApiKeyCard from './ApiKey'; +import ConnectorCard from './Connector'; + +export default function Dashboard() { + const { slug } = useParams(); + const { user } = useUser(); + const [loading, setLoading] = useState(true); + const [organizations, setOrganizations] = useState([]); + const [organization, setOrganization] = useState(null); + const [connector, setConnector] = useState(false); + const [workspaces, setWorkspaces] = useState([]); + + useEffect(() => { + async function userOrgs() { + const orgs = await User.organizations(); + if (orgs.length === 0) { + window.location.replace(paths.onboarding.orgName()); + return false; + } + + if (!slug) { + window.location.replace(paths.organization(orgs?.[0])); + return false; + } + + const focusedOrg = + orgs?.find((org: any) => org.slug === slug) || orgs?.[0]; + const _workspaces = await Organization.workspaces(focusedOrg.slug); + const _connector = await Organization.connector(focusedOrg.slug); + setOrganizations(orgs); + setOrganization(focusedOrg); + setWorkspaces(_workspaces); + setConnector(_connector); + setLoading(false); + } + userOrgs(); + }, [user.uid, window.location.pathname]); + + if (loading || organizations.length === 0 || !organization) { + return ( + + + + ); + } + + return ( + + {!!organization && ( +
    + + +
    + )} + + +
    +
    + +
    + +
    +
    + ); +} diff --git a/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx b/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx new file mode 100644 index 0000000..54f2a1c --- /dev/null +++ b/frontend/src/pages/DocumentView/FragmentList/DeleteEmbeddingConfirmation/index.tsx @@ -0,0 +1,73 @@ +import { memo, useState } from 'react'; +import Document from '../../../../models/document'; +import { Loader } from 'react-feather'; + +const DeleteEmbeddingConfirmation = memo( + ({ data, fragment }: { data: any; fragment: any }) => { + const [loading, setLoading] = useState(false); + if (!data || !fragment) return null; + + const deleteEmbedding = async () => { + setLoading(true); + const { success } = await Document.deleteFragment(fragment.id); + if (success) { + document + ?.getElementById(`embedding-row-${fragment.id}`) + ?.classList.add('hidden'); + document?.getElementById(`${fragment.id}-delete-embedding`)?.close(); + } + setLoading(false); + }; + + return ( + +
    +

    + Delete this embedding? +

    +

    + Once you delete this embedding it will remove it from your connected + Vector Database as well. This process is non-reversible and if you + want to add it back will require you to manually insert it or + re-embed the document. +

    +
    +
    +
    +            {data.metadata.text}
    +          
    +
    + + +
    +
    +
    + ); + } +); + +export default DeleteEmbeddingConfirmation; diff --git a/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx b/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx new file mode 100644 index 0000000..f73f8c2 --- /dev/null +++ b/frontend/src/pages/DocumentView/FragmentList/EditEmbeddingConfirmation/index.tsx @@ -0,0 +1,209 @@ +import { memo, useEffect, useRef, useState } from 'react'; +import Document from '../../../../models/document'; +import { AlertTriangle, Loader } from 'react-feather'; +import { APP_NAME } from '../../../../utils/constants'; +import System from '../../../../models/system'; +import debounce from 'lodash/debounce'; +import { validEmbedding, MAX_TOKENS } from '../../../../utils/tokenizer'; +import { numberWithCommas } from '../../../../utils/numbers'; + +const EditEmbeddingConfirmation = memo( + ({ + data, + fragment, + canEdit, + }: { + data: any; + fragment: any; + canEdit: boolean; + }) => { + const inputEl = useRef(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [tokenLength, setTokenLength] = useState( + validEmbedding(data?.metadata?.text).length || 0 + ); + + const updateSystemSetting = async (e: any) => { + e.preventDefault(); + const form = new FormData(e.target); + const open_ai_api_key = form.get('open_ai_api_key'); + await System.updateSettings({ open_ai_api_key }); + window.location.reload(); + }; + + const checkTokenSize = (e: any) => { + const { length, valid } = validEmbedding(e.target.value); + setTokenLength(length); + + if (length === 0) { + setError('Text cannot be empty.'); + return false; + } + valid ? setError(null) : setError('Text is too long to embed.'); + return valid; + }; + const handleSubmit = async (e: any) => { + e.preventDefault(); + setLoading(true); + const form = new FormData(e.target); + const newText = form.get('embeddingText'); + const { success, error } = await Document.updateFragment( + fragment.id, + newText + ); + if (success) + document?.getElementById(`${fragment.id}-edit-embedding`)?.close(); + setError(error); + setLoading(false); + }; + + useEffect(() => { + function updateHeight(el: HTMLInputElement) { + el.style.height = el.value.split('\n').length * 20 + 'px'; + } + !!inputEl?.current && updateHeight(inputEl.current); + }, []); + + if (!canEdit) { + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
    +

    + Edit embedding +

    +

    + You can edit your embedding chunk to be more inclusive of a chunk + of text if it was split incorrectly or simply just to provide more + context. +

    +
    +
    +
    +
    + You cannot edit embeddings without an OpenAI + API key set for your instance. +
    +

    + {APP_NAME} currently only supports editing and changes of + embeddings using OpenAI text embedding. If you did not embed + this data originally using OpenAI you will be unable to use this + feature. +

    +
    +
    +
    + + +
    +
    + +
    +
    +

    + This key will only be used for the embedding of changes or + additions you make via {APP_NAME}. +

    +
    +
    +
    +
    +            {data.metadata.text}
    +          
    +
    + ); + } + + const debouncedTokenLengthCheck = debounce(checkTokenSize, 500); + return ( + + event.target == event.currentTarget && event.currentTarget?.close() + } + > +
    +

    Edit embedding

    +

    + You can edit your embedding chunk to be more inclusive of a chunk of + text if it was split incorrectly or simply just to provide more + context. +

    +
    +
    +
    +

    {error || ''}

    +

    + {numberWithCommas(tokenLength)}/ + {numberWithCommas(MAX_TOKENS.cl100k_base)}{' '} +

    +
    +
    +