Compare commits
5 Commits
2e813846dc
...
d72f1af361
Author | SHA1 | Date |
---|---|---|
Timothy Carambat | d72f1af361 | |
Sean Hatfield | 360f17cd58 | |
Sean Hatfield | 8eda75d624 | |
Timothy Carambat | 1b35bcbeab | |
timothycarambat | df2c01b176 |
|
@ -261,8 +261,8 @@ function Directory({
|
|||
)}
|
||||
</div>
|
||||
{amountSelected !== 0 && (
|
||||
<div className="absolute bottom-[12px] left-0 right-0 flex justify-center">
|
||||
<div className="mx-auto bg-white/40 rounded-lg py-1 px-2">
|
||||
<div className="absolute bottom-[12px] left-0 right-0 flex justify-center pointer-events-none">
|
||||
<div className="mx-auto bg-white/40 rounded-lg py-1 px-2 pointer-events-auto">
|
||||
<div className="flex flex-row items-center gap-x-2">
|
||||
<button
|
||||
onClick={moveToWorkspace}
|
||||
|
@ -306,6 +306,7 @@ function Directory({
|
|||
workspace={workspace}
|
||||
fetchKeys={fetchKeys}
|
||||
setLoading={setLoading}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -7,18 +7,37 @@ import PreLoader from "../../../../../Preloader";
|
|||
|
||||
function FileUploadProgressComponent({
|
||||
slug,
|
||||
uuid,
|
||||
file,
|
||||
setFiles,
|
||||
rejected = false,
|
||||
reason = null,
|
||||
onUploadSuccess,
|
||||
onUploadError,
|
||||
setLoading,
|
||||
setLoadingMessage,
|
||||
}) {
|
||||
const [timerMs, setTimerMs] = useState(10);
|
||||
const [status, setStatus] = useState("pending");
|
||||
const [error, setError] = useState("");
|
||||
const [isFadingOut, setIsFadingOut] = useState(false);
|
||||
|
||||
const fadeOut = (cb) => {
|
||||
setIsFadingOut(true);
|
||||
cb?.();
|
||||
};
|
||||
|
||||
const beginFadeOut = () => {
|
||||
setIsFadingOut(false);
|
||||
setFiles((prev) => {
|
||||
return prev.filter((item) => item.uid !== uuid);
|
||||
});
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
async function uploadFile() {
|
||||
setLoading(true);
|
||||
setLoadingMessage("Uploading file...");
|
||||
const start = Number(new Date());
|
||||
const formData = new FormData();
|
||||
formData.append("file", file, file.name);
|
||||
|
@ -34,17 +53,28 @@ function FileUploadProgressComponent({
|
|||
onUploadError(data.error);
|
||||
setError(data.error);
|
||||
} else {
|
||||
setLoading(false);
|
||||
setLoadingMessage("");
|
||||
setStatus("complete");
|
||||
clearInterval(timer);
|
||||
onUploadSuccess();
|
||||
}
|
||||
|
||||
// Begin fadeout timer to clear uploader queue.
|
||||
setTimeout(() => {
|
||||
fadeOut(() => setTimeout(() => beginFadeOut(), 300));
|
||||
}, 5000);
|
||||
}
|
||||
!!file && !rejected && uploadFile();
|
||||
}, []);
|
||||
|
||||
if (rejected) {
|
||||
return (
|
||||
<div className="h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40">
|
||||
<div
|
||||
className={`${
|
||||
isFadingOut ? "file-upload-fadeout" : "file-upload"
|
||||
} h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40`}
|
||||
>
|
||||
<div className="w-6 h-6 flex-shrink-0">
|
||||
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
||||
</div>
|
||||
|
@ -60,7 +90,11 @@ function FileUploadProgressComponent({
|
|||
|
||||
if (status === "failed") {
|
||||
return (
|
||||
<div className="h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40 overflow-y-auto">
|
||||
<div
|
||||
className={`${
|
||||
isFadingOut ? "file-upload-fadeout" : "file-upload"
|
||||
} h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40 overflow-y-auto`}
|
||||
>
|
||||
<div className="w-6 h-6 flex-shrink-0">
|
||||
<XCircle className="w-6 h-6 stroke-white bg-red-500 rounded-full p-1 w-full h-full" />
|
||||
</div>
|
||||
|
@ -75,7 +109,11 @@ function FileUploadProgressComponent({
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40">
|
||||
<div
|
||||
className={`${
|
||||
isFadingOut ? "file-upload-fadeout" : "file-upload"
|
||||
} h-14 px-2 py-2 flex items-center gap-x-4 rounded-lg bg-white/5 border border-white/40`}
|
||||
>
|
||||
<div className="w-6 h-6 flex-shrink-0">
|
||||
{status !== "complete" ? (
|
||||
<div className="flex items-center justify-center">
|
||||
|
|
|
@ -6,8 +6,14 @@ import { useDropzone } from "react-dropzone";
|
|||
import { v4 } from "uuid";
|
||||
import FileUploadProgress from "./FileUploadProgress";
|
||||
import Workspace from "../../../../../models/workspace";
|
||||
import debounce from "lodash.debounce";
|
||||
|
||||
export default function UploadFile({ workspace, fetchKeys, setLoading }) {
|
||||
export default function UploadFile({
|
||||
workspace,
|
||||
fetchKeys,
|
||||
setLoading,
|
||||
setLoadingMessage,
|
||||
}) {
|
||||
const [ready, setReady] = useState(false);
|
||||
const [files, setFiles] = useState([]);
|
||||
const [fetchingUrl, setFetchingUrl] = useState(false);
|
||||
|
@ -15,6 +21,7 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
|
|||
const handleSendLink = async (e) => {
|
||||
e.preventDefault();
|
||||
setLoading(true);
|
||||
setLoadingMessage("Scraping link...");
|
||||
setFetchingUrl(true);
|
||||
const formEl = e.target;
|
||||
const form = new FormData(formEl);
|
||||
|
@ -33,14 +40,9 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
|
|||
setFetchingUrl(false);
|
||||
};
|
||||
|
||||
const handleUploadSuccess = () => {
|
||||
fetchKeys(true);
|
||||
showToast("File uploaded successfully", "success", { clear: true });
|
||||
};
|
||||
|
||||
const handleUploadError = (message) => {
|
||||
showToast(`Error uploading file: ${message}`, "error");
|
||||
};
|
||||
// Don't spam fetchKeys, wait 1s between calls at least.
|
||||
const handleUploadSuccess = debounce(() => fetchKeys(true), 1000);
|
||||
const handleUploadError = (_msg) => null; // stubbed.
|
||||
|
||||
const onDrop = async (acceptedFiles, rejections) => {
|
||||
const newAccepted = acceptedFiles.map((file) => {
|
||||
|
@ -109,11 +111,15 @@ export default function UploadFile({ workspace, fetchKeys, setLoading }) {
|
|||
<FileUploadProgress
|
||||
key={file.uid}
|
||||
file={file.file}
|
||||
uuid={file.uid}
|
||||
setFiles={setFiles}
|
||||
slug={workspace.slug}
|
||||
rejected={file?.rejected}
|
||||
reason={file?.reason}
|
||||
onUploadSuccess={handleUploadSuccess}
|
||||
onUploadError={handleUploadError}
|
||||
setLoading={setLoading}
|
||||
setLoadingMessage={setLoadingMessage}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
|
|
@ -692,3 +692,53 @@ does not extend the close button beyond the viewport. */
|
|||
.text-tremor-content {
|
||||
padding-bottom: 10px;
|
||||
}
|
||||
|
||||
.file-upload {
|
||||
-webkit-animation: fadein 0.3s linear forwards;
|
||||
animation: fadein 0.3s linear forwards;
|
||||
}
|
||||
|
||||
.file-upload-fadeout {
|
||||
-webkit-animation: fadeout 0.3s linear forwards;
|
||||
animation: fadeout 0.3s linear forwards;
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadein {
|
||||
0% {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes fadeout {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeout {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,23 @@ const prisma = require("../utils/prisma");
|
|||
const { EventLogs } = require("./eventLogs");
|
||||
|
||||
const User = {
|
||||
writable: [
|
||||
// Used for generic updates so we can validate keys in request body
|
||||
"username",
|
||||
"password",
|
||||
"pfpFilename",
|
||||
"role",
|
||||
"suspended",
|
||||
],
|
||||
// validations for the above writable fields.
|
||||
castColumnValue: function (key, value) {
|
||||
switch (key) {
|
||||
case "suspended":
|
||||
return Number(Boolean(value));
|
||||
default:
|
||||
return String(value);
|
||||
}
|
||||
},
|
||||
create: async function ({ username, password, role = "default" }) {
|
||||
const passwordCheck = this.checkPasswordComplexity(password);
|
||||
if (!passwordCheck.checkedOK) {
|
||||
|
@ -42,13 +59,26 @@ const User = {
|
|||
|
||||
update: async function (userId, updates = {}) {
|
||||
try {
|
||||
if (!userId) throw new Error("No user id provided for update");
|
||||
const currentUser = await prisma.users.findUnique({
|
||||
where: { id: parseInt(userId) },
|
||||
});
|
||||
if (!currentUser) {
|
||||
return { success: false, error: "User not found" };
|
||||
}
|
||||
if (!currentUser) return { success: false, error: "User not found" };
|
||||
|
||||
// Removes non-writable fields for generic updates
|
||||
// and force-casts to the proper type;
|
||||
Object.entries(updates).forEach(([key, value]) => {
|
||||
if (this.writable.includes(key)) {
|
||||
updates[key] = this.castColumnValue(key, value);
|
||||
return;
|
||||
}
|
||||
delete updates[key];
|
||||
});
|
||||
|
||||
if (Object.keys(updates).length === 0)
|
||||
return { success: false, error: "No valid updates applied." };
|
||||
|
||||
// Handle password specific updates
|
||||
if (updates.hasOwnProperty("password")) {
|
||||
const passwordCheck = this.checkPasswordComplexity(updates.password);
|
||||
if (!passwordCheck.checkedOK) {
|
||||
|
@ -78,6 +108,24 @@ const User = {
|
|||
}
|
||||
},
|
||||
|
||||
// Explicit direct update of user object.
|
||||
// Only use this method when directly setting a key value
|
||||
// that takes no user input for the keys being modified.
|
||||
_update: async function (id = null, data = {}) {
|
||||
if (!id) throw new Error("No user id provided for update");
|
||||
|
||||
try {
|
||||
const user = await prisma.users.update({
|
||||
where: { id },
|
||||
data,
|
||||
});
|
||||
return { user, message: null };
|
||||
} catch (error) {
|
||||
console.error(error.message);
|
||||
return { user: null, message: error.message };
|
||||
}
|
||||
},
|
||||
|
||||
get: async function (clause = {}) {
|
||||
try {
|
||||
const user = await prisma.users.findFirst({ where: clause });
|
||||
|
|
|
@ -8,6 +8,11 @@ const {
|
|||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { safeJsonParse } = require("../../http");
|
||||
const cacheFolder = path.resolve(
|
||||
process.env.STORAGE_DIR
|
||||
? path.resolve(process.env.STORAGE_DIR, "models", "openrouter")
|
||||
: path.resolve(__dirname, `../../../storage/models/openrouter`)
|
||||
);
|
||||
|
||||
class OpenRouterLLM {
|
||||
constructor(embedder = null, modelPreference = null) {
|
||||
|
@ -38,12 +43,8 @@ class OpenRouterLLM {
|
|||
this.embedder = !embedder ? new NativeEmbedder() : embedder;
|
||||
this.defaultTemp = 0.7;
|
||||
|
||||
const cacheFolder = path.resolve(
|
||||
process.env.STORAGE_DIR
|
||||
? path.resolve(process.env.STORAGE_DIR, "models", "openrouter")
|
||||
: path.resolve(__dirname, `../../../storage/models/openrouter`)
|
||||
);
|
||||
fs.mkdirSync(cacheFolder, { recursive: true });
|
||||
if (!fs.existsSync(cacheFolder))
|
||||
fs.mkdirSync(cacheFolder, { recursive: true });
|
||||
this.cacheModelPath = path.resolve(cacheFolder, "models.json");
|
||||
this.cacheAtPath = path.resolve(cacheFolder, ".cached_at");
|
||||
}
|
||||
|
@ -52,11 +53,6 @@ class OpenRouterLLM {
|
|||
console.log(`\x1b[36m[${this.constructor.name}]\x1b[0m ${text}`, ...args);
|
||||
}
|
||||
|
||||
async init() {
|
||||
await this.#syncModels();
|
||||
return this;
|
||||
}
|
||||
|
||||
// This checks if the .cached_at file has a timestamp that is more than 1Week (in millis)
|
||||
// from the current date. If it is, then we will refetch the API so that all the models are up
|
||||
// to date.
|
||||
|
@ -80,37 +76,7 @@ class OpenRouterLLM {
|
|||
this.log(
|
||||
"Model cache is not present or stale. Fetching from OpenRouter API."
|
||||
);
|
||||
await fetch(`${this.basePath}/models`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ data = [] }) => {
|
||||
const models = {};
|
||||
data.forEach((model) => {
|
||||
models[model.id] = {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
organization:
|
||||
model.id.split("/")[0].charAt(0).toUpperCase() +
|
||||
model.id.split("/")[0].slice(1),
|
||||
maxLength: model.context_length,
|
||||
};
|
||||
});
|
||||
fs.writeFileSync(this.cacheModelPath, JSON.stringify(models), {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
fs.writeFileSync(this.cacheAtPath, String(Number(new Date())), {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
return models;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return {};
|
||||
});
|
||||
await fetchOpenRouterModels();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -420,6 +386,54 @@ class OpenRouterLLM {
|
|||
}
|
||||
}
|
||||
|
||||
async function fetchOpenRouterModels() {
|
||||
return await fetch(`https://openrouter.ai/api/v1/models`, {
|
||||
method: "GET",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
})
|
||||
.then((res) => res.json())
|
||||
.then(({ data = [] }) => {
|
||||
const models = {};
|
||||
data.forEach((model) => {
|
||||
models[model.id] = {
|
||||
id: model.id,
|
||||
name: model.name,
|
||||
organization:
|
||||
model.id.split("/")[0].charAt(0).toUpperCase() +
|
||||
model.id.split("/")[0].slice(1),
|
||||
maxLength: model.context_length,
|
||||
};
|
||||
});
|
||||
|
||||
// Cache all response information
|
||||
if (!fs.existsSync(cacheFolder))
|
||||
fs.mkdirSync(cacheFolder, { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.resolve(cacheFolder, "models.json"),
|
||||
JSON.stringify(models),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.resolve(cacheFolder, ".cached_at"),
|
||||
String(Number(new Date())),
|
||||
{
|
||||
encoding: "utf-8",
|
||||
}
|
||||
);
|
||||
|
||||
return models;
|
||||
})
|
||||
.catch((e) => {
|
||||
console.error(e);
|
||||
return {};
|
||||
});
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
OpenRouterLLM,
|
||||
fetchOpenRouterModels,
|
||||
};
|
||||
|
|
|
@ -22,7 +22,7 @@ async function generateRecoveryCodes(userId) {
|
|||
const { error } = await RecoveryCode.createMany(newRecoveryCodes);
|
||||
if (!!error) throw new Error(error);
|
||||
|
||||
const { success } = await User.update(userId, {
|
||||
const { user: success } = await User._update(userId, {
|
||||
seen_recovery_codes: true,
|
||||
});
|
||||
if (!success) throw new Error("Failed to generate user recovery codes!");
|
||||
|
@ -80,6 +80,11 @@ async function resetPassword(token, _newPassword = "", confirmPassword = "") {
|
|||
// JOI password rules will be enforced inside .update.
|
||||
const { error } = await User.update(resetToken.user_id, {
|
||||
password: newPassword,
|
||||
});
|
||||
|
||||
// seen_recovery_codes is not publicly writable
|
||||
// so we have to do direct update here
|
||||
await User._update(resetToken.user_id, {
|
||||
seen_recovery_codes: false,
|
||||
});
|
||||
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
const { OpenRouterLLM } = require("../AiProviders/openRouter");
|
||||
const {
|
||||
OpenRouterLLM,
|
||||
fetchOpenRouterModels,
|
||||
} = require("../AiProviders/openRouter");
|
||||
const { perplexityModels } = require("../AiProviders/perplexity");
|
||||
const { togetherAiModels } = require("../AiProviders/togetherAi");
|
||||
const SUPPORT_CUSTOM_MODELS = [
|
||||
|
@ -232,8 +235,7 @@ async function getPerplexityModels() {
|
|||
}
|
||||
|
||||
async function getOpenRouterModels() {
|
||||
const openrouter = await new OpenRouterLLM().init();
|
||||
const knownModels = openrouter.models();
|
||||
const knownModels = await fetchOpenRouterModels();
|
||||
if (!Object.keys(knownModels).length === 0)
|
||||
return { models: [], error: null };
|
||||
|
||||
|
|
Loading…
Reference in New Issue