GitHub loader extension + extension support v1 (#469)
* feat: implement github repo loading fix: purge of folders fix: rendering of sub-files * noshow delete on custom-documents * Add API key support because of rate limits * WIP for frontend of data connectors * wip * Add frontend form for GitHub repo data connector * remove console.logs block custom-documents from being deleted * remove _meta unused arg * Add support for ignore pathing in request Ignore path input via tagging * Update hint
This commit is contained in:
parent
2d700b13f6
commit
452582489e
52
collector/extensions/index.js
Normal file
52
collector/extensions/index.js
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
const { reqBody } = require("../utils/http");
|
||||||
|
|
||||||
|
function extensions(app) {
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
|
app.post("/ext/github-repo", async function (request, response) {
|
||||||
|
try {
|
||||||
|
const loadGithubRepo = require("../utils/extensions/GithubRepo");
|
||||||
|
const { success, reason, data } = await loadGithubRepo(reqBody(request));
|
||||||
|
response.status(200).json({
|
||||||
|
success,
|
||||||
|
reason,
|
||||||
|
data
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.status(200).json({
|
||||||
|
success: false,
|
||||||
|
reason: e.message || "A processing error occurred.",
|
||||||
|
data: {},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
|
||||||
|
// gets all branches for a specific repo
|
||||||
|
app.post("/ext/github-repo/branches", async function (request, response) {
|
||||||
|
try {
|
||||||
|
const GithubRepoLoader = require("../utils/extensions/GithubRepo/RepoLoader");
|
||||||
|
const allBranches = await (new GithubRepoLoader(reqBody(request))).getRepoBranches()
|
||||||
|
response.status(200).json({
|
||||||
|
success: true,
|
||||||
|
reason: null,
|
||||||
|
data: {
|
||||||
|
branches: allBranches
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.status(400).json({
|
||||||
|
success: false,
|
||||||
|
reason: e.message,
|
||||||
|
data: {
|
||||||
|
branches: []
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = extensions;
|
||||||
@ -11,6 +11,7 @@ const { reqBody } = require("./utils/http");
|
|||||||
const { processSingleFile } = require("./processSingleFile");
|
const { processSingleFile } = require("./processSingleFile");
|
||||||
const { processLink } = require("./processLink");
|
const { processLink } = require("./processLink");
|
||||||
const { wipeCollectorStorage } = require("./utils/files");
|
const { wipeCollectorStorage } = require("./utils/files");
|
||||||
|
const extensions = require("./extensions");
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(cors({ origin: true }));
|
app.use(cors({ origin: true }));
|
||||||
@ -57,6 +58,8 @@ app.post("/process-link", async function (request, response) {
|
|||||||
return;
|
return;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
extensions(app);
|
||||||
|
|
||||||
app.get("/accepts", function (_, response) {
|
app.get("/accepts", function (_, response) {
|
||||||
response.status(200).json(ACCEPTED_MIMES);
|
response.status(200).json(ACCEPTED_MIMES);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -24,6 +24,7 @@
|
|||||||
"express": "^4.18.2",
|
"express": "^4.18.2",
|
||||||
"extract-zip": "^2.0.1",
|
"extract-zip": "^2.0.1",
|
||||||
"fluent-ffmpeg": "^2.1.2",
|
"fluent-ffmpeg": "^2.1.2",
|
||||||
|
"ignore": "^5.3.0",
|
||||||
"js-tiktoken": "^1.0.8",
|
"js-tiktoken": "^1.0.8",
|
||||||
"langchain": "0.0.201",
|
"langchain": "0.0.201",
|
||||||
"mammoth": "^1.6.0",
|
"mammoth": "^1.6.0",
|
||||||
@ -35,6 +36,7 @@
|
|||||||
"pdf-parse": "^1.1.1",
|
"pdf-parse": "^1.1.1",
|
||||||
"puppeteer": "^21.6.1",
|
"puppeteer": "^21.6.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
|
"url-pattern": "^1.0.3",
|
||||||
"uuid": "^9.0.0",
|
"uuid": "^9.0.0",
|
||||||
"wavefile": "^11.0.0"
|
"wavefile": "^11.0.0"
|
||||||
},
|
},
|
||||||
@ -42,4 +44,4 @@
|
|||||||
"nodemon": "^2.0.22",
|
"nodemon": "^2.0.22",
|
||||||
"prettier": "^2.4.1"
|
"prettier": "^2.4.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
149
collector/utils/extensions/GithubRepo/RepoLoader/index.js
Normal file
149
collector/utils/extensions/GithubRepo/RepoLoader/index.js
Normal file
@ -0,0 +1,149 @@
|
|||||||
|
class RepoLoader {
|
||||||
|
constructor(args = {}) {
|
||||||
|
this.ready = false;
|
||||||
|
this.repo = args?.repo;
|
||||||
|
this.branch = args?.branch;
|
||||||
|
this.accessToken = args?.accessToken || null;
|
||||||
|
this.ignorePaths = args?.ignorePaths || [];
|
||||||
|
|
||||||
|
this.author = null;
|
||||||
|
this.project = null;
|
||||||
|
this.branches = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
#validGithubUrl() {
|
||||||
|
const UrlPattern = require("url-pattern");
|
||||||
|
const pattern = new UrlPattern("https\\://github.com/(:author)/(:project)");
|
||||||
|
const match = pattern.match(this.repo);
|
||||||
|
if (!match) return false;
|
||||||
|
|
||||||
|
this.author = match.author;
|
||||||
|
this.project = match.project;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure the branch provided actually exists
|
||||||
|
// and if it does not or has not been set auto-assign to primary branch.
|
||||||
|
async #validBranch() {
|
||||||
|
await this.getRepoBranches();
|
||||||
|
if (!!this.branch && this.branches.includes(this.branch)) return;
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
"[Github Loader]: Branch not set! Auto-assigning to a default branch."
|
||||||
|
);
|
||||||
|
this.branch = this.branches.includes("main") ? "main" : "master";
|
||||||
|
console.log(`[Github Loader]: Branch auto-assigned to ${this.branch}.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async #validateAccessToken() {
|
||||||
|
if (!this.accessToken) return;
|
||||||
|
const valid = await fetch("https://api.github.com/octocat", {
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
Authorization: `Bearer ${this.accessToken}`,
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.ok) throw new Error(res.statusText);
|
||||||
|
return res.ok;
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(
|
||||||
|
"Invalid Github Access Token provided! Access token will not be used",
|
||||||
|
e.message
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!valid) this.accessToken = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
async init() {
|
||||||
|
if (!this.#validGithubUrl()) return;
|
||||||
|
await this.#validBranch();
|
||||||
|
await this.#validateAccessToken();
|
||||||
|
this.ready = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async recursiveLoader() {
|
||||||
|
if (!this.ready) throw new Error("[Github Loader]: not in ready state!");
|
||||||
|
const {
|
||||||
|
GithubRepoLoader: LCGithubLoader,
|
||||||
|
} = require("langchain/document_loaders/web/github");
|
||||||
|
|
||||||
|
if (this.accessToken)
|
||||||
|
console.log(
|
||||||
|
`[Github Loader]: Access token set! Recursive loading enabled!`
|
||||||
|
);
|
||||||
|
|
||||||
|
const loader = new LCGithubLoader(this.repo, {
|
||||||
|
accessToken: this.accessToken,
|
||||||
|
branch: this.branch,
|
||||||
|
recursive: !!this.accessToken, // Recursive will hit rate limits.
|
||||||
|
maxConcurrency: 5,
|
||||||
|
unknown: "ignore",
|
||||||
|
ignorePaths: this.ignorePaths,
|
||||||
|
});
|
||||||
|
|
||||||
|
const docs = [];
|
||||||
|
for await (const doc of loader.loadAsStream()) docs.push(doc);
|
||||||
|
return docs;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sort branches to always show either main or master at the top of the result.
|
||||||
|
#branchPrefSort(branches = []) {
|
||||||
|
const preferredSort = ["main", "master"];
|
||||||
|
return branches.reduce((acc, branch) => {
|
||||||
|
if (preferredSort.includes(branch)) return [branch, ...acc];
|
||||||
|
return [...acc, branch];
|
||||||
|
}, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all branches for a given repo.
|
||||||
|
async getRepoBranches() {
|
||||||
|
if (!this.#validGithubUrl() || !this.author || !this.project) return [];
|
||||||
|
await this.#validateAccessToken(); // Ensure API access token is valid for pre-flight
|
||||||
|
|
||||||
|
let page = 0;
|
||||||
|
let polling = true;
|
||||||
|
const branches = [];
|
||||||
|
|
||||||
|
while (polling) {
|
||||||
|
console.log(`Fetching page ${page} of branches for ${this.project}`);
|
||||||
|
await fetch(
|
||||||
|
`https://api.github.com/repos/${this.author}/${this.project}/branches?per_page=100&page=${page}`,
|
||||||
|
{
|
||||||
|
method: "GET",
|
||||||
|
headers: {
|
||||||
|
...(this.accessToken
|
||||||
|
? { Authorization: `Bearer ${this.accessToken}` }
|
||||||
|
: {}),
|
||||||
|
"X-GitHub-Api-Version": "2022-11-28",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.then((res) => {
|
||||||
|
if (res.ok) return res.json();
|
||||||
|
throw new Error(`Invalid request to Github API: ${res.statusText}`);
|
||||||
|
})
|
||||||
|
.then((branchObjects) => {
|
||||||
|
polling = branchObjects.length > 0;
|
||||||
|
branches.push(branchObjects.map((branch) => branch.name));
|
||||||
|
page++;
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
polling = false;
|
||||||
|
console.log(`RepoLoader.branches`, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.branches = [...new Set(branches.flat())];
|
||||||
|
return this.#branchPrefSort(this.branches);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = RepoLoader;
|
||||||
78
collector/utils/extensions/GithubRepo/index.js
Normal file
78
collector/utils/extensions/GithubRepo/index.js
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
const RepoLoader = require("./RepoLoader");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
const { default: slugify } = require("slugify");
|
||||||
|
const { v4 } = require("uuid");
|
||||||
|
const { writeToServerDocuments } = require("../../files");
|
||||||
|
const { tokenizeString } = require("../../tokenizer");
|
||||||
|
|
||||||
|
async function loadGithubRepo(args) {
|
||||||
|
const repo = new RepoLoader(args);
|
||||||
|
await repo.init();
|
||||||
|
|
||||||
|
if (!repo.ready)
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: "Could not prepare Github repo for loading! Check URL",
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`-- Working Github ${repo.author}/${repo.project}:${repo.branch} --`
|
||||||
|
);
|
||||||
|
const docs = await repo.recursiveLoader();
|
||||||
|
if (!docs.length) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
reason: "No files were found for those settings.",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`[Github Loader]: Found ${docs.length} source files. Saving...`);
|
||||||
|
const outFolder = slugify(
|
||||||
|
`${repo.author}-${repo.project}-${repo.branch}-${v4().slice(0, 4)}`
|
||||||
|
).toLowerCase();
|
||||||
|
const outFolderPath = path.resolve(
|
||||||
|
__dirname,
|
||||||
|
`../../../../server/storage/documents/${outFolder}`
|
||||||
|
);
|
||||||
|
fs.mkdirSync(outFolderPath);
|
||||||
|
|
||||||
|
for (const doc of docs) {
|
||||||
|
if (!doc.pageContent) continue;
|
||||||
|
const data = {
|
||||||
|
id: v4(),
|
||||||
|
url: "github://" + doc.metadata.source,
|
||||||
|
title: doc.metadata.source,
|
||||||
|
docAuthor: repo.author,
|
||||||
|
description: "No description found.",
|
||||||
|
docSource: repo.repo,
|
||||||
|
chunkSource: doc.metadata.source,
|
||||||
|
published: new Date().toLocaleString(),
|
||||||
|
wordCount: doc.pageContent.split(" ").length,
|
||||||
|
pageContent: doc.pageContent,
|
||||||
|
token_count_estimate: tokenizeString(doc.pageContent).length,
|
||||||
|
};
|
||||||
|
console.log(
|
||||||
|
`[Github Loader]: Saving ${doc.metadata.source} to ${outFolder}`
|
||||||
|
);
|
||||||
|
writeToServerDocuments(
|
||||||
|
data,
|
||||||
|
`${slugify(doc.metadata.source)}-${data.id}`,
|
||||||
|
outFolderPath
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
reason: null,
|
||||||
|
data: {
|
||||||
|
author: repo.author,
|
||||||
|
repo: repo.project,
|
||||||
|
branch: repo.branch,
|
||||||
|
files: docs.length,
|
||||||
|
destination: outFolder,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = loadGithubRepo;
|
||||||
@ -1530,6 +1530,11 @@ ignore-by-default@^1.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
resolved "https://registry.yarnpkg.com/ignore-by-default/-/ignore-by-default-1.0.1.tgz#48ca6d72f6c6a3af00a9ad4ae6876be3889e2b09"
|
||||||
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
integrity sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA==
|
||||||
|
|
||||||
|
ignore@^5.3.0:
|
||||||
|
version "5.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.3.0.tgz#67418ae40d34d6999c95ff56016759c718c82f78"
|
||||||
|
integrity sha512-g7dmpshy+gD7mh88OC9NwSGTKoc3kyLAZQRU1mt53Aw/vnvfXnbC+F/7F7QoYVKbV+KNvJx8wArewKy1vXMtlg==
|
||||||
|
|
||||||
immediate@~3.0.5:
|
immediate@~3.0.5:
|
||||||
version "3.0.6"
|
version "3.0.6"
|
||||||
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
|
||||||
@ -3127,6 +3132,11 @@ unpipe@1.0.0, unpipe@~1.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||||
|
|
||||||
|
url-pattern@^1.0.3:
|
||||||
|
version "1.0.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1"
|
||||||
|
integrity sha512-uQcEj/2puA4aq1R3A2+VNVBgaWYR24FdWjl7VNW83rnWftlhyzOZ/tBjezRiC2UkIzuxC8Top3IekN3vUf1WxA==
|
||||||
|
|
||||||
url-template@^2.0.8:
|
url-template@^2.0.8:
|
||||||
version "2.0.8"
|
version "2.0.8"
|
||||||
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
|
resolved "https://registry.yarnpkg.com/url-template/-/url-template-2.0.8.tgz#fc565a3cccbff7730c775f5641f9555791439f21"
|
||||||
|
|||||||
@ -27,6 +27,7 @@
|
|||||||
"react-loading-icons": "^1.1.0",
|
"react-loading-icons": "^1.1.0",
|
||||||
"react-loading-skeleton": "^3.1.0",
|
"react-loading-skeleton": "^3.1.0",
|
||||||
"react-router-dom": "^6.3.0",
|
"react-router-dom": "^6.3.0",
|
||||||
|
"react-tag-input-component": "^2.0.2",
|
||||||
"react-toastify": "^9.1.3",
|
"react-toastify": "^9.1.3",
|
||||||
"text-case": "^1.0.9",
|
"text-case": "^1.0.9",
|
||||||
"truncate": "^3.0.0",
|
"truncate": "^3.0.0",
|
||||||
|
|||||||
@ -36,6 +36,12 @@ const GeneralExportImport = lazy(() =>
|
|||||||
import("@/pages/GeneralSettings/ExportImport")
|
import("@/pages/GeneralSettings/ExportImport")
|
||||||
);
|
);
|
||||||
const GeneralSecurity = lazy(() => import("@/pages/GeneralSettings/Security"));
|
const GeneralSecurity = lazy(() => import("@/pages/GeneralSettings/Security"));
|
||||||
|
const DataConnectors = lazy(() =>
|
||||||
|
import("@/pages/GeneralSettings/DataConnectors")
|
||||||
|
);
|
||||||
|
const DataConnectorSetup = lazy(() =>
|
||||||
|
import("@/pages/GeneralSettings/DataConnectors/Connectors")
|
||||||
|
);
|
||||||
const OnboardingFlow = lazy(() => import("@/pages/OnboardingFlow"));
|
const OnboardingFlow = lazy(() => import("@/pages/OnboardingFlow"));
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
@ -103,6 +109,15 @@ export default function App() {
|
|||||||
path="/settings/workspaces"
|
path="/settings/workspaces"
|
||||||
element={<ManagerRoute Component={AdminWorkspaces} />}
|
element={<ManagerRoute Component={AdminWorkspaces} />}
|
||||||
/>
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings/data-connectors"
|
||||||
|
element={<ManagerRoute Component={DataConnectors} />}
|
||||||
|
/>
|
||||||
|
<Route
|
||||||
|
path="/settings/data-connectors/:connector"
|
||||||
|
element={<ManagerRoute Component={DataConnectorSetup} />}
|
||||||
|
/>
|
||||||
|
|
||||||
{/* Onboarding Flow */}
|
{/* Onboarding Flow */}
|
||||||
<Route path="/onboarding" element={<OnboardingFlow />} />
|
<Route path="/onboarding" element={<OnboardingFlow />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
|
|||||||
39
frontend/src/components/DataConnectorOption/index.jsx
Normal file
39
frontend/src/components/DataConnectorOption/index.jsx
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import paths from "@/utils/paths";
|
||||||
|
import ConnectorImages from "./media";
|
||||||
|
|
||||||
|
export default function DataConnectorOption({ slug }) {
|
||||||
|
if (!DATA_CONNECTORS.hasOwnProperty(slug)) return null;
|
||||||
|
const { path, image, name, description, link } = DATA_CONNECTORS[slug];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<a href={path}>
|
||||||
|
<label className="transition-all duration-300 inline-flex flex-col h-full w-60 cursor-pointer items-start justify-between rounded-2xl bg-preference-gradient border-2 border-transparent shadow-md px-5 py-4 text-white hover:bg-selected-preference-gradient hover:border-white/60 peer-checked:border-white peer-checked:border-opacity-90 peer-checked:bg-selected-preference-gradient">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<img src={image} alt={name} className="h-10 w-10 rounded" />
|
||||||
|
<div className="ml-4 text-sm font-semibold">{name}</div>
|
||||||
|
</div>
|
||||||
|
<div className="mt-2 text-xs font-base text-white tracking-wide">
|
||||||
|
{description}
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href={link}
|
||||||
|
target="_blank"
|
||||||
|
className="mt-2 text-xs text-white font-medium underline"
|
||||||
|
>
|
||||||
|
{link}
|
||||||
|
</a>
|
||||||
|
</label>
|
||||||
|
</a>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DATA_CONNECTORS = {
|
||||||
|
github: {
|
||||||
|
name: "GitHub Repo",
|
||||||
|
path: paths.settings.dataConnectors.github(),
|
||||||
|
image: ConnectorImages.github,
|
||||||
|
description:
|
||||||
|
"Import an entire public or private Github repository in a single click.",
|
||||||
|
link: "https://github.com",
|
||||||
|
},
|
||||||
|
};
|
||||||
BIN
frontend/src/components/DataConnectorOption/media/github.png
Normal file
BIN
frontend/src/components/DataConnectorOption/media/github.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 22 KiB |
@ -0,0 +1,5 @@
|
|||||||
|
import Github from "./github.png";
|
||||||
|
const ConnectorImages = {
|
||||||
|
github: Github,
|
||||||
|
};
|
||||||
|
export default ConnectorImages;
|
||||||
@ -33,7 +33,7 @@ export default function FileRow({
|
|||||||
try {
|
try {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setLoadingMessage("This may take a while for large documents");
|
setLoadingMessage("This may take a while for large documents");
|
||||||
await System.deleteDocument(`${folderName}/${item.name}`, item);
|
await System.deleteDocument(`${folderName}/${item.name}`);
|
||||||
await fetchKeys(true);
|
await fetchKeys(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Failed to delete the document:", error);
|
console.error("Failed to delete the document:", error);
|
||||||
@ -60,7 +60,7 @@ export default function FileRow({
|
|||||||
selected ? "bg-sky-500/20" : ""
|
selected ? "bg-sky-500/20" : ""
|
||||||
} ${expanded ? "bg-sky-500/10" : ""}`}`}
|
} ${expanded ? "bg-sky-500/10" : ""}`}`}
|
||||||
>
|
>
|
||||||
<div className="col-span-4 flex gap-x-[4px] items-center">
|
<div className="pl-4 col-span-4 flex gap-x-[4px] items-center">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
|
|||||||
@ -1,7 +1,8 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import FileRow from "../FileRow";
|
import FileRow from "../FileRow";
|
||||||
import { CaretDown, FolderNotch } from "@phosphor-icons/react";
|
import { CaretDown, FolderNotch, Trash } from "@phosphor-icons/react";
|
||||||
import { middleTruncate } from "@/utils/directories";
|
import { middleTruncate } from "@/utils/directories";
|
||||||
|
import System from "@/models/system";
|
||||||
|
|
||||||
export default function FolderRow({
|
export default function FolderRow({
|
||||||
item,
|
item,
|
||||||
@ -12,8 +13,32 @@ export default function FolderRow({
|
|||||||
fetchKeys,
|
fetchKeys,
|
||||||
setLoading,
|
setLoading,
|
||||||
setLoadingMessage,
|
setLoadingMessage,
|
||||||
|
autoExpanded = false,
|
||||||
}) {
|
}) {
|
||||||
const [expanded, setExpanded] = useState(true);
|
const [expanded, setExpanded] = useState(autoExpanded);
|
||||||
|
|
||||||
|
const onTrashClick = async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
if (
|
||||||
|
!window.confirm(
|
||||||
|
"Are you sure you want to delete this folder?\nThis will require you to re-upload and re-embed it.\nAny documents in this folder will be removed from any workspace that is currently referencing it.\nThis action is not reversible."
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
setLoadingMessage("This may take a while for large folders");
|
||||||
|
await System.deleteFolder(item.name);
|
||||||
|
await fetchKeys(true);
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to delete the document:", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selected) toggleSelection(item);
|
||||||
|
setLoading(false);
|
||||||
|
};
|
||||||
|
|
||||||
const handleExpandClick = (event) => {
|
const handleExpandClick = (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
@ -30,7 +55,7 @@ export default function FolderRow({
|
|||||||
>
|
>
|
||||||
<div className="col-span-4 flex gap-x-[4px] items-center">
|
<div className="col-span-4 flex gap-x-[4px] items-center">
|
||||||
<div
|
<div
|
||||||
className="w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
className="shrink-0 w-3 h-3 rounded border-[1px] border-white flex justify-center items-center cursor-pointer"
|
||||||
role="checkbox"
|
role="checkbox"
|
||||||
aria-checked={selected}
|
aria-checked={selected}
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
@ -46,7 +71,7 @@ export default function FolderRow({
|
|||||||
<CaretDown className="text-base font-bold w-4 h-4" />
|
<CaretDown className="text-base font-bold w-4 h-4" />
|
||||||
</div>
|
</div>
|
||||||
<FolderNotch
|
<FolderNotch
|
||||||
className="text-base font-bold w-4 h-4 mr-[3px]"
|
className="shrink-0 text-base font-bold w-4 h-4 mr-[3px]"
|
||||||
weight="fill"
|
weight="fill"
|
||||||
/>
|
/>
|
||||||
<p className="whitespace-nowrap overflow-show">
|
<p className="whitespace-nowrap overflow-show">
|
||||||
@ -56,7 +81,14 @@ export default function FolderRow({
|
|||||||
<p className="col-span-2 pl-3.5" />
|
<p className="col-span-2 pl-3.5" />
|
||||||
<p className="col-span-2 pl-3" />
|
<p className="col-span-2 pl-3" />
|
||||||
<p className="col-span-2 pl-2" />
|
<p className="col-span-2 pl-2" />
|
||||||
<div className="col-span-2 flex justify-end items-center" />
|
<div className="col-span-2 flex justify-end items-center">
|
||||||
|
{item.name !== "custom-documents" && (
|
||||||
|
<Trash
|
||||||
|
onClick={onTrashClick}
|
||||||
|
className="text-base font-bold w-4 h-4 ml-2 flex-shrink-0 cursor-pointer"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{expanded && (
|
{expanded && (
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
|
|||||||
@ -106,6 +106,7 @@ export default function Directory({
|
|||||||
isSelected={isSelected}
|
isSelected={isSelected}
|
||||||
setLoading={setLoading}
|
setLoading={setLoading}
|
||||||
setLoadingMessage={setLoadingMessage}
|
setLoadingMessage={setLoadingMessage}
|
||||||
|
autoExpanded={index === 0}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import {
|
|||||||
X,
|
X,
|
||||||
List,
|
List,
|
||||||
FileCode,
|
FileCode,
|
||||||
|
Plugs,
|
||||||
} from "@phosphor-icons/react";
|
} from "@phosphor-icons/react";
|
||||||
import useUser from "@/hooks/useUser";
|
import useUser from "@/hooks/useUser";
|
||||||
import { USER_BACKGROUND_COLOR } from "@/utils/constants";
|
import { USER_BACKGROUND_COLOR } from "@/utils/constants";
|
||||||
@ -127,6 +128,11 @@ export default function SettingsSidebar() {
|
|||||||
btnText="Vector Database"
|
btnText="Vector Database"
|
||||||
icon={<Database className="h-5 w-5 flex-shrink-0" />}
|
icon={<Database className="h-5 w-5 flex-shrink-0" />}
|
||||||
/>
|
/>
|
||||||
|
<Option
|
||||||
|
href={paths.settings.dataConnectors.list()}
|
||||||
|
btnText="Data Connectors"
|
||||||
|
icon={<Plugs className="h-5 w-5 flex-shrink-0" />}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@ -1,34 +0,0 @@
|
|||||||
import pluralize from "pluralize";
|
|
||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import System from "@/models/system";
|
|
||||||
import { numberWithCommas } from "@/utils/numbers";
|
|
||||||
|
|
||||||
export default function IndexCount() {
|
|
||||||
const [indexes, setIndexes] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
async function indexCount() {
|
|
||||||
setIndexes(await System.totalIndexes());
|
|
||||||
}
|
|
||||||
indexCount();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (indexes === null || indexes === 0) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-end gap-x-2">
|
|
||||||
<div className="flex items-center gap-x-1 px-2 rounded-full">
|
|
||||||
<p className="text-slate-400 leading-tight text-sm"></p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-end gap-x-2">
|
|
||||||
<div className="flex items-center gap-x-1 px-2 rounded-full">
|
|
||||||
<p className="text-slate-400 leading-tight text-sm">
|
|
||||||
{numberWithCommas(indexes)} {pluralize("vector", indexes)}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -1,49 +0,0 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
|
||||||
import { WarningCircle, Circle } from "@phosphor-icons/react";
|
|
||||||
import System from "@/models/system";
|
|
||||||
|
|
||||||
export default function LLMStatus() {
|
|
||||||
const [status, setStatus] = useState(null);
|
|
||||||
useEffect(() => {
|
|
||||||
async function checkPing() {
|
|
||||||
setStatus(await System.ping());
|
|
||||||
}
|
|
||||||
checkPing();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (status === null) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-start gap-x-2">
|
|
||||||
<p className="text-slate-400 leading-loose text-sm">LLM</p>
|
|
||||||
<div className="flex items-center gap-x-1 border border-slate-400 px-2 rounded-full">
|
|
||||||
<p className="text-slate-400 leading-tight text-sm">unknown</p>
|
|
||||||
<Circle className="h-3 w-3 stroke-slate-700 fill-slate-400 animate-pulse" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: add modal or toast on click to identify why this is broken
|
|
||||||
// need to likely start server.
|
|
||||||
if (status === false) {
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-end gap-x-2">
|
|
||||||
<p className="text-slate-400 leading-loose text-sm">LLM</p>
|
|
||||||
<div className="flex items-center gap-x-1 border border-red-400 px-2 bg-red-200 rounded-full">
|
|
||||||
<p className="text-red-700 leading-tight text-sm">offline</p>
|
|
||||||
<WarningCircle className="h-3 w-3 stroke-red-100 fill-red-400" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="flex w-full items-center justify-end gap-x-2">
|
|
||||||
<p className="text-slate-400 leading-loose text-sm">LLM</p>
|
|
||||||
<div className="flex items-center gap-x-1 border border-slate-400 px-2 rounded-full">
|
|
||||||
<p className="text-slate-400 leading-tight text-sm">online</p>
|
|
||||||
<Circle className="h-3 w-3 stroke-green-100 fill-green-400 animate-pulse" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@ -71,25 +71,6 @@ export default function Sidebar() {
|
|||||||
<ActiveWorkspaces />
|
<ActiveWorkspaces />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col flex-grow justify-end mb-2">
|
<div className="flex flex-col flex-grow justify-end mb-2">
|
||||||
{/* <div className="flex flex-col gap-y-2">
|
|
||||||
<div className="w-full flex items-center justify-between">
|
|
||||||
<LLMStatus />
|
|
||||||
<IndexCount />
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
href={paths.feedback()}
|
|
||||||
target="_blank"
|
|
||||||
className="flex flex-grow w-[100%] h-[36px] gap-x-2 py-[5px] px-4 border border-transparent rounded-lg text-slate-200 justify-center items-center bg-stone-800 hover:bg-stone-900"
|
|
||||||
>
|
|
||||||
<AtSign className="h-4 w-4" />
|
|
||||||
<p className="text-slate-200 text-xs leading-loose font-semibold">
|
|
||||||
Feedback form
|
|
||||||
</p>
|
|
||||||
</a>
|
|
||||||
<ManagedHosting />
|
|
||||||
<LogoutButton />
|
|
||||||
</div> */}
|
|
||||||
|
|
||||||
{/* Footer */}
|
{/* Footer */}
|
||||||
<div className="flex justify-center mt-2">
|
<div className="flex justify-center mt-2">
|
||||||
<div className="flex space-x-4">
|
<div className="flex space-x-4">
|
||||||
|
|||||||
@ -385,3 +385,7 @@ dialog::backdrop {
|
|||||||
@apply border-blue-500 bg-blue-400/10 text-blue-800;
|
@apply border-blue-500 bg-blue-400/10 text-blue-800;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.rti--container {
|
||||||
|
@apply !bg-zinc-900 !text-white !placeholder-white !placeholder-opacity-60 !text-sm !rounded-lg !p-2.5;
|
||||||
|
}
|
||||||
|
|||||||
47
frontend/src/models/dataConnector.js
Normal file
47
frontend/src/models/dataConnector.js
Normal file
@ -0,0 +1,47 @@
|
|||||||
|
import { API_BASE } from "@/utils/constants";
|
||||||
|
import { baseHeaders } from "@/utils/request";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
|
||||||
|
const DataConnector = {
|
||||||
|
github: {
|
||||||
|
branches: async ({ repo, accessToken }) => {
|
||||||
|
return await fetch(`${API_BASE}/ext/github/branches`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
cache: "force-cache",
|
||||||
|
body: JSON.stringify({ repo, accessToken }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.success) throw new Error(res.reason);
|
||||||
|
return res.data;
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
return { branches: data?.branches || [], error: null };
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
showToast(e.message, "error");
|
||||||
|
return { branches: [], error: e.message };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
collect: async function ({ repo, accessToken, branch, ignorePaths = [] }) {
|
||||||
|
return await fetch(`${API_BASE}/ext/github/repo`, {
|
||||||
|
method: "POST",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify({ repo, accessToken, branch, ignorePaths }),
|
||||||
|
})
|
||||||
|
.then((res) => res.json())
|
||||||
|
.then((res) => {
|
||||||
|
if (!res.success) throw new Error(res.reason);
|
||||||
|
return { data: res.data, error: null };
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return { data: null, error: e.message };
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default DataConnector;
|
||||||
@ -1,5 +1,6 @@
|
|||||||
import { API_BASE, AUTH_TIMESTAMP } from "@/utils/constants";
|
import { API_BASE, AUTH_TIMESTAMP } from "@/utils/constants";
|
||||||
import { baseHeaders } from "@/utils/request";
|
import { baseHeaders } from "@/utils/request";
|
||||||
|
import DataConnector from "./dataConnector";
|
||||||
|
|
||||||
const System = {
|
const System = {
|
||||||
ping: async function () {
|
ping: async function () {
|
||||||
@ -133,11 +134,23 @@ const System = {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
deleteDocument: async (name, meta) => {
|
deleteDocument: async (name) => {
|
||||||
return await fetch(`${API_BASE}/system/remove-document`, {
|
return await fetch(`${API_BASE}/system/remove-document`, {
|
||||||
method: "DELETE",
|
method: "DELETE",
|
||||||
headers: baseHeaders(),
|
headers: baseHeaders(),
|
||||||
body: JSON.stringify({ name, meta }),
|
body: JSON.stringify({ name }),
|
||||||
|
})
|
||||||
|
.then((res) => res.ok)
|
||||||
|
.catch((e) => {
|
||||||
|
console.error(e);
|
||||||
|
return false;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
deleteFolder: async (name) => {
|
||||||
|
return await fetch(`${API_BASE}/system/remove-folder`, {
|
||||||
|
method: "DELETE",
|
||||||
|
headers: baseHeaders(),
|
||||||
|
body: JSON.stringify({ name }),
|
||||||
})
|
})
|
||||||
.then((res) => res.ok)
|
.then((res) => res.ok)
|
||||||
.catch((e) => {
|
.catch((e) => {
|
||||||
@ -431,6 +444,7 @@ const System = {
|
|||||||
return { success: false, error: e.message };
|
return { success: false, error: e.message };
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
dataConnectors: DataConnector,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default System;
|
export default System;
|
||||||
|
|||||||
@ -0,0 +1,294 @@
|
|||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import { DATA_CONNECTORS } from "@/components/DataConnectorOption";
|
||||||
|
import System from "@/models/system";
|
||||||
|
import { Info } from "@phosphor-icons/react/dist/ssr";
|
||||||
|
import showToast from "@/utils/toast";
|
||||||
|
import pluralize from "pluralize";
|
||||||
|
import { TagsInput } from "react-tag-input-component";
|
||||||
|
|
||||||
|
const DEFAULT_BRANCHES = ["main", "master"];
|
||||||
|
export default function GithubConnectorSetup() {
|
||||||
|
const { image } = DATA_CONNECTORS.github;
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [repo, setRepo] = useState(null);
|
||||||
|
const [accessToken, setAccessToken] = useState(null);
|
||||||
|
const [ignores, setIgnores] = useState([]);
|
||||||
|
|
||||||
|
const [settings, setSettings] = useState({
|
||||||
|
repo: null,
|
||||||
|
accessToken: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSubmit = async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const form = new FormData(e.target);
|
||||||
|
|
||||||
|
try {
|
||||||
|
setLoading(true);
|
||||||
|
showToast(
|
||||||
|
"Fetching all files for repo - this may take a while.",
|
||||||
|
"info",
|
||||||
|
{ clear: true, autoClose: false }
|
||||||
|
);
|
||||||
|
const { data, error } = await System.dataConnectors.github.collect({
|
||||||
|
repo: form.get("repo"),
|
||||||
|
accessToken: form.get("accessToken"),
|
||||||
|
branch: form.get("branch"),
|
||||||
|
ignorePaths: ignores,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!!error) {
|
||||||
|
showToast(error, "error", { clear: true });
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
showToast(
|
||||||
|
`${data.files} ${pluralize("file", data.files)} collected from ${
|
||||||
|
data.author
|
||||||
|
}/${data.repo}:${data.branch}. Output folder is ${data.destination}.`,
|
||||||
|
"success",
|
||||||
|
{ clear: true }
|
||||||
|
);
|
||||||
|
e.target.reset();
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
showToast(e.message, "error", { clear: true });
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
|
{!isMobile && <Sidebar />}
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
|
||||||
|
>
|
||||||
|
{isMobile && <SidebarMobileHeader />}
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="flex w-full gap-x-4 items-center pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<img src={image} alt="Github" className="rounded-lg h-16 w-16" />
|
||||||
|
<div className="w-full flex flex-col gap-y-1">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
|
Import GitHub Repository
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
Import all files from a public or private Github repository
|
||||||
|
and have its files be available in your workspace.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form className="w-full" onSubmit={handleSubmit}>
|
||||||
|
{!accessToken && (
|
||||||
|
<div className="flex flex-col gap-y-1 py-4 ">
|
||||||
|
<div className="flex flex-col w-fit gap-y-2 bg-blue-600/20 rounded-lg px-4 py-2">
|
||||||
|
<div className="flex items-center gap-x-2">
|
||||||
|
<Info size={20} className="shrink-0 text-blue-400" />
|
||||||
|
<p className="text-blue-400 text-sm">
|
||||||
|
Trying to collect a GitHub repo without a{" "}
|
||||||
|
<a
|
||||||
|
href="https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/managing-your-personal-access-tokens"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className="underline"
|
||||||
|
>
|
||||||
|
Personal Access Token
|
||||||
|
</a>{" "}
|
||||||
|
will fail to collect all files due to GitHub API limits.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
href="https://github.com/settings/personal-access-tokens/new"
|
||||||
|
rel="noreferrer"
|
||||||
|
target="_blank"
|
||||||
|
className="text-blue-400 hover:underline"
|
||||||
|
>
|
||||||
|
Create a temporary Access Token for this data connector
|
||||||
|
→
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="w-full flex flex-col py-2">
|
||||||
|
<div className="w-full flex items-center gap-4">
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
|
<label className="text-white text-sm font-semibold block">
|
||||||
|
GitHub Repo URL
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-zinc-300">
|
||||||
|
Url of the GitHub repo you wish to collect.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="url"
|
||||||
|
name="repo"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="https://github.com/Mintplex-Labs/anything-llm"
|
||||||
|
required={true}
|
||||||
|
autoComplete="off"
|
||||||
|
onChange={(e) => setRepo(e.target.value)}
|
||||||
|
onBlur={() => setSettings({ ...settings, repo })}
|
||||||
|
spellCheck={false}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
|
<label className="text-white text-sm block flex gap-x-2 items-center">
|
||||||
|
<p className="font-semibold ">Github Access Token</p>{" "}
|
||||||
|
<p className="text-xs text-zinc-300 font-base!">
|
||||||
|
<i>optional</i>
|
||||||
|
</p>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-zinc-300 flex gap-x-2">
|
||||||
|
Access Token to prevent rate limiting.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
name="accessToken"
|
||||||
|
className="bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white block w-full p-2.5"
|
||||||
|
placeholder="github_pat_1234_abcdefg"
|
||||||
|
required={false}
|
||||||
|
autoComplete="off"
|
||||||
|
spellCheck={false}
|
||||||
|
onChange={(e) => setAccessToken(e.target.value)}
|
||||||
|
onBlur={() => setSettings({ ...settings, accessToken })}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<GitHubBranchSelection
|
||||||
|
repo={settings.repo}
|
||||||
|
accessToken={settings.accessToken}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col w-1/2 py-4">
|
||||||
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
|
<label className="text-white text-sm block flex gap-x-2 items-center">
|
||||||
|
<p className="font-semibold ">File Ignores</p>
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-zinc-300 flex gap-x-2">
|
||||||
|
List in .gitignore format to ignore specific files during
|
||||||
|
collection. Press enter after each entry you want to save.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<TagsInput
|
||||||
|
value={ignores}
|
||||||
|
onChange={setIgnores}
|
||||||
|
name="ignores"
|
||||||
|
placeholder="!*.js, images/*, .DS_Store, bin/*"
|
||||||
|
classNames={{
|
||||||
|
tag: "bg-blue-300/10 text-zinc-800 m-1",
|
||||||
|
input:
|
||||||
|
"flex bg-zinc-900 text-white placeholder-white placeholder-opacity-60 text-sm rounded-lg focus:border-white p-2.5",
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-y-2 w-fit">
|
||||||
|
<button
|
||||||
|
type="submit"
|
||||||
|
disabled={loading}
|
||||||
|
className="mt-2 text-lg w-fit border border-slate-200 px-4 py-1 rounded-lg text-slate-200 items-center flex gap-x-2 hover:bg-slate-200 hover:text-slate-800 disabled:bg-slate-200 disabled:text-slate-800"
|
||||||
|
>
|
||||||
|
{loading
|
||||||
|
? "Collecting files..."
|
||||||
|
: "Collect all files from GitHub repo"}
|
||||||
|
</button>
|
||||||
|
{loading && (
|
||||||
|
<p className="text-xs text-zinc-300">
|
||||||
|
Once complete, all files will be available for embedding
|
||||||
|
into workspaces in the document picker.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function GitHubBranchSelection({ repo, accessToken }) {
|
||||||
|
const [allBranches, setAllBranches] = useState(DEFAULT_BRANCHES);
|
||||||
|
const [loading, setLoading] = useState(true);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function fetchAllBranches() {
|
||||||
|
if (!repo) {
|
||||||
|
setAllBranches(DEFAULT_BRANCHES);
|
||||||
|
setLoading(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setLoading(true);
|
||||||
|
const { branches } = await System.dataConnectors.github.branches({
|
||||||
|
repo,
|
||||||
|
accessToken,
|
||||||
|
});
|
||||||
|
setAllBranches(branches.length > 0 ? branches : DEFAULT_BRANCHES);
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
fetchAllBranches();
|
||||||
|
}, [repo, accessToken]);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
|
<label className="text-white text-sm font-semibold block">
|
||||||
|
Branch
|
||||||
|
</label>
|
||||||
|
<p className="text-xs text-zinc-300">
|
||||||
|
Branch you wish to collect files of
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
name="branch"
|
||||||
|
required={true}
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
>
|
||||||
|
<option disabled={true} selected={true}>
|
||||||
|
-- loading available models --
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col w-60">
|
||||||
|
<div className="flex flex-col gap-y-1 mb-4">
|
||||||
|
<label className="text-white text-sm font-semibold block">Branch</label>
|
||||||
|
<p className="text-xs text-zinc-300">
|
||||||
|
Branch you wish to collect files of
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<select
|
||||||
|
name="branch"
|
||||||
|
required={true}
|
||||||
|
className="bg-zinc-900 border border-gray-500 text-white text-sm rounded-lg block w-full p-2.5"
|
||||||
|
>
|
||||||
|
{allBranches.map((branch) => {
|
||||||
|
return (
|
||||||
|
<option key={branch} value={branch}>
|
||||||
|
{branch}
|
||||||
|
</option>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -0,0 +1,19 @@
|
|||||||
|
import paths from "@/utils/paths";
|
||||||
|
import { lazy } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
const Github = lazy(() => import("./Github"));
|
||||||
|
|
||||||
|
const CONNECTORS = {
|
||||||
|
github: Github,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function DataConnectorSetup() {
|
||||||
|
const { connector } = useParams();
|
||||||
|
if (!connector || !CONNECTORS.hasOwnProperty(connector)) {
|
||||||
|
window.location = paths.home();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Page = CONNECTORS[connector];
|
||||||
|
return <Page />;
|
||||||
|
}
|
||||||
38
frontend/src/pages/GeneralSettings/DataConnectors/index.jsx
Normal file
38
frontend/src/pages/GeneralSettings/DataConnectors/index.jsx
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
import React from "react";
|
||||||
|
import Sidebar, { SidebarMobileHeader } from "@/components/SettingsSidebar";
|
||||||
|
import { isMobile } from "react-device-detect";
|
||||||
|
import DataConnectorOption from "@/components/DataConnectorOption";
|
||||||
|
|
||||||
|
export default function DataConnectors() {
|
||||||
|
return (
|
||||||
|
<div className="w-screen h-screen overflow-hidden bg-sidebar flex">
|
||||||
|
{!isMobile && <Sidebar />}
|
||||||
|
<div
|
||||||
|
style={{ height: isMobile ? "100%" : "calc(100% - 32px)" }}
|
||||||
|
className="relative md:ml-[2px] md:mr-[16px] md:my-[16px] md:rounded-[26px] bg-main-gradient w-full h-full overflow-y-scroll border-4 border-accent"
|
||||||
|
>
|
||||||
|
{isMobile && <SidebarMobileHeader />}
|
||||||
|
<div className="flex w-full">
|
||||||
|
<div className="flex flex-col w-full px-1 md:px-20 md:py-12 py-16">
|
||||||
|
<div className="w-full flex flex-col gap-y-1 pb-6 border-white border-b-2 border-opacity-10">
|
||||||
|
<div className="items-center flex gap-x-4">
|
||||||
|
<p className="text-2xl font-semibold text-white">
|
||||||
|
Data Connectors
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<p className="text-sm font-base text-white text-opacity-60">
|
||||||
|
Verified data connectors allow you to add more content to your
|
||||||
|
AnythingLLM workspaces with no custom code or complexity.
|
||||||
|
<br />
|
||||||
|
Guaranteed to work with your AnythingLLM instance.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="py-4 w-full flex md:flex-wrap overflow-x-scroll gap-4 max-w-full">
|
||||||
|
<DataConnectorOption slug="github" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@ -76,5 +76,13 @@ export default {
|
|||||||
apiKeys: () => {
|
apiKeys: () => {
|
||||||
return "/settings/api-keys";
|
return "/settings/api-keys";
|
||||||
},
|
},
|
||||||
|
dataConnectors: {
|
||||||
|
list: () => {
|
||||||
|
return "/settings/data-connectors";
|
||||||
|
},
|
||||||
|
github: () => {
|
||||||
|
return "/settings/data-connectors/github";
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -2061,6 +2061,11 @@ react-router@6.12.1:
|
|||||||
dependencies:
|
dependencies:
|
||||||
"@remix-run/router" "1.6.3"
|
"@remix-run/router" "1.6.3"
|
||||||
|
|
||||||
|
react-tag-input-component@^2.0.2:
|
||||||
|
version "2.0.2"
|
||||||
|
resolved "https://registry.yarnpkg.com/react-tag-input-component/-/react-tag-input-component-2.0.2.tgz#f62f013c6a535141dd1c6c3a88858223170150f1"
|
||||||
|
integrity sha512-dydI9luVwwv9vrjE5u1TTnkcOVkOVL6mhFti8r6hLi78V2F2EKWQOLptURz79UYbDHLSk6tnbvGl8FE+sMpADg==
|
||||||
|
|
||||||
react-toastify@^9.1.3:
|
react-toastify@^9.1.3:
|
||||||
version "9.1.3"
|
version "9.1.3"
|
||||||
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff"
|
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-9.1.3.tgz#1e798d260d606f50e0fab5ee31daaae1d628c5ff"
|
||||||
|
|||||||
53
server/endpoints/extensions/index.js
Normal file
53
server/endpoints/extensions/index.js
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
const { Telemetry } = require("../../models/telemetry");
|
||||||
|
const {
|
||||||
|
forwardExtensionRequest,
|
||||||
|
} = require("../../utils/files/documentProcessor");
|
||||||
|
const {
|
||||||
|
flexUserRoleValid,
|
||||||
|
} = require("../../utils/middleware/multiUserProtected");
|
||||||
|
const { validatedRequest } = require("../../utils/middleware/validatedRequest");
|
||||||
|
|
||||||
|
function extensionEndpoints(app) {
|
||||||
|
if (!app) return;
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/ext/github/branches",
|
||||||
|
[validatedRequest, flexUserRoleValid],
|
||||||
|
async (request, response) => {
|
||||||
|
try {
|
||||||
|
const responseFromProcessor = await forwardExtensionRequest({
|
||||||
|
endpoint: "/ext/github-repo/branches",
|
||||||
|
method: "POST",
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
response.status(200).json(responseFromProcessor);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.post(
|
||||||
|
"/ext/github/repo",
|
||||||
|
[validatedRequest, flexUserRoleValid],
|
||||||
|
async (request, response) => {
|
||||||
|
try {
|
||||||
|
const responseFromProcessor = await forwardExtensionRequest({
|
||||||
|
endpoint: "/ext/github-repo",
|
||||||
|
method: "POST",
|
||||||
|
body: request.body,
|
||||||
|
});
|
||||||
|
await Telemetry.sendTelemetry("extension_invoked", {
|
||||||
|
type: "github_repo",
|
||||||
|
});
|
||||||
|
response.status(200).json(responseFromProcessor);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = { extensionEndpoints };
|
||||||
@ -7,7 +7,7 @@ const {
|
|||||||
checkProcessorAlive,
|
checkProcessorAlive,
|
||||||
acceptedFileTypes,
|
acceptedFileTypes,
|
||||||
} = require("../utils/files/documentProcessor");
|
} = require("../utils/files/documentProcessor");
|
||||||
const { purgeDocument } = require("../utils/files/purgeDocument");
|
const { purgeDocument, purgeFolder } = require("../utils/files/purgeDocument");
|
||||||
const { getVectorDbClass } = require("../utils/helpers");
|
const { getVectorDbClass } = require("../utils/helpers");
|
||||||
const { updateENV, dumpENV } = require("../utils/helpers/updateENV");
|
const { updateENV, dumpENV } = require("../utils/helpers/updateENV");
|
||||||
const {
|
const {
|
||||||
@ -196,8 +196,23 @@ function systemEndpoints(app) {
|
|||||||
[validatedRequest],
|
[validatedRequest],
|
||||||
async (request, response) => {
|
async (request, response) => {
|
||||||
try {
|
try {
|
||||||
const { name, meta } = reqBody(request);
|
const { name } = reqBody(request);
|
||||||
await purgeDocument(name, meta);
|
await purgeDocument(name);
|
||||||
|
response.sendStatus(200).end();
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e.message, e);
|
||||||
|
response.sendStatus(500).end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
app.delete(
|
||||||
|
"/system/remove-folder",
|
||||||
|
[validatedRequest],
|
||||||
|
async (request, response) => {
|
||||||
|
try {
|
||||||
|
const { name } = reqBody(request);
|
||||||
|
await purgeFolder(name);
|
||||||
response.sendStatus(200).end();
|
response.sendStatus(200).end();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.log(e.message, e);
|
console.log(e.message, e);
|
||||||
|
|||||||
@ -18,6 +18,7 @@ const { utilEndpoints } = require("./endpoints/utils");
|
|||||||
const { Telemetry } = require("./models/telemetry");
|
const { Telemetry } = require("./models/telemetry");
|
||||||
const { developerEndpoints } = require("./endpoints/api");
|
const { developerEndpoints } = require("./endpoints/api");
|
||||||
const setupTelemetry = require("./utils/telemetry");
|
const setupTelemetry = require("./utils/telemetry");
|
||||||
|
const { extensionEndpoints } = require("./endpoints/extensions");
|
||||||
const app = express();
|
const app = express();
|
||||||
const apiRouter = express.Router();
|
const apiRouter = express.Router();
|
||||||
const FILE_LIMIT = "3GB";
|
const FILE_LIMIT = "3GB";
|
||||||
@ -34,6 +35,7 @@ app.use(
|
|||||||
|
|
||||||
app.use("/api", apiRouter);
|
app.use("/api", apiRouter);
|
||||||
systemEndpoints(apiRouter);
|
systemEndpoints(apiRouter);
|
||||||
|
extensionEndpoints(apiRouter);
|
||||||
workspaceEndpoints(apiRouter);
|
workspaceEndpoints(apiRouter);
|
||||||
chatEndpoints(apiRouter);
|
chatEndpoints(apiRouter);
|
||||||
adminEndpoints(apiRouter);
|
adminEndpoints(apiRouter);
|
||||||
|
|||||||
@ -59,9 +59,32 @@ async function processLink(link = "") {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We will not ever expose the document processor to the frontend API so instead we relay
|
||||||
|
// all requests through the server. You can use this function to directly expose a specific endpoint
|
||||||
|
// on the document processor.
|
||||||
|
async function forwardExtensionRequest({ endpoint, method, body }) {
|
||||||
|
return await fetch(`${PROCESSOR_API}${endpoint}`, {
|
||||||
|
method,
|
||||||
|
body, // Stringified JSON!
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.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, data: {}, reason: e.message };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
checkProcessorAlive,
|
checkProcessorAlive,
|
||||||
processDocument,
|
processDocument,
|
||||||
processLink,
|
processLink,
|
||||||
acceptedFileTypes,
|
acceptedFileTypes,
|
||||||
|
forwardExtensionRequest,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -144,18 +144,14 @@ async function storeVectorResult(vectorData = [], filename = null) {
|
|||||||
// Purges a file from the documents/ folder.
|
// Purges a file from the documents/ folder.
|
||||||
async function purgeSourceDocument(filename = null) {
|
async function purgeSourceDocument(filename = null) {
|
||||||
if (!filename) return;
|
if (!filename) return;
|
||||||
console.log(`Purging document of ${filename}.`);
|
console.log(`Purging source document of ${filename}.`);
|
||||||
|
|
||||||
const filePath =
|
const filePath =
|
||||||
process.env.NODE_ENV === "development"
|
process.env.NODE_ENV === "development"
|
||||||
? path.resolve(__dirname, `../../storage/documents`, filename)
|
? path.resolve(__dirname, `../../storage/documents`, filename)
|
||||||
: path.resolve(process.env.STORAGE_DIR, `documents`, filename);
|
: path.resolve(process.env.STORAGE_DIR, `documents`, filename);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) return;
|
||||||
console.log(`Could not located cachefile for ${filename}`, filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.rmSync(filePath);
|
fs.rmSync(filePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -163,7 +159,7 @@ async function purgeSourceDocument(filename = null) {
|
|||||||
// Purges a vector-cache file from the vector-cache/ folder.
|
// Purges a vector-cache file from the vector-cache/ folder.
|
||||||
async function purgeVectorCache(filename = null) {
|
async function purgeVectorCache(filename = null) {
|
||||||
if (!filename) return;
|
if (!filename) return;
|
||||||
console.log(`Purging cached vectorized results of ${filename}.`);
|
console.log(`Purging vector-cache of ${filename}.`);
|
||||||
|
|
||||||
const digest = uuidv5(filename, uuidv5.URL);
|
const digest = uuidv5(filename, uuidv5.URL);
|
||||||
const filePath =
|
const filePath =
|
||||||
@ -171,11 +167,7 @@ async function purgeVectorCache(filename = null) {
|
|||||||
? path.resolve(__dirname, `../../storage/vector-cache`, `${digest}.json`)
|
? path.resolve(__dirname, `../../storage/vector-cache`, `${digest}.json`)
|
||||||
: path.resolve(process.env.STORAGE_DIR, `vector-cache`, `${digest}.json`);
|
: path.resolve(process.env.STORAGE_DIR, `vector-cache`, `${digest}.json`);
|
||||||
|
|
||||||
if (!fs.existsSync(filePath)) {
|
if (!fs.existsSync(filePath)) return;
|
||||||
console.log(`Could not located cache file for ${filename}`, filePath);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs.rmSync(filePath);
|
fs.rmSync(filePath);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
const { purgeVectorCache, purgeSourceDocument } = require(".");
|
const { purgeVectorCache, purgeSourceDocument } = require(".");
|
||||||
const { Document } = require("../../models/documents");
|
const { Document } = require("../../models/documents");
|
||||||
const { Workspace } = require("../../models/workspace");
|
const { Workspace } = require("../../models/workspace");
|
||||||
|
|
||||||
async function purgeDocument(filename, meta) {
|
async function purgeDocument(filename) {
|
||||||
const workspaces = await Workspace.where();
|
const workspaces = await Workspace.where();
|
||||||
for (const workspace of workspaces) {
|
for (const workspace of workspaces) {
|
||||||
await Document.removeDocuments(workspace, [filename]);
|
await Document.removeDocuments(workspace, [filename]);
|
||||||
@ -12,6 +15,45 @@ async function purgeDocument(filename, meta) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function purgeFolder(folderName) {
|
||||||
|
if (folderName === "custom-documents") return;
|
||||||
|
const documentsFolder =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? path.resolve(__dirname, `../../storage/documents`)
|
||||||
|
: path.resolve(process.env.STORAGE_DIR, `documents`);
|
||||||
|
|
||||||
|
const folderPath = path.resolve(documentsFolder, folderName);
|
||||||
|
const filenames = fs
|
||||||
|
.readdirSync(folderPath)
|
||||||
|
.map((file) => path.join(folderName, file));
|
||||||
|
const workspaces = await Workspace.where();
|
||||||
|
|
||||||
|
const purgePromises = [];
|
||||||
|
// Remove associated Vector-cache files
|
||||||
|
for (const filename of filenames) {
|
||||||
|
const rmVectorCache = () =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
purgeVectorCache(filename).then(() => resolve(true))
|
||||||
|
);
|
||||||
|
purgePromises.push(rmVectorCache);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove workspace document associations
|
||||||
|
for (const workspace of workspaces) {
|
||||||
|
const rmWorkspaceDoc = () =>
|
||||||
|
new Promise((resolve) =>
|
||||||
|
Document.removeDocuments(workspace, filenames).then(() => resolve(true))
|
||||||
|
);
|
||||||
|
purgePromises.push(rmWorkspaceDoc);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(purgePromises.flat().map((f) => f()));
|
||||||
|
fs.rmSync(folderPath, { recursive: true }); // Delete root document and source files.
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
purgeDocument,
|
purgeDocument,
|
||||||
|
purgeFolder,
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user