Compare commits

...

5 Commits

Author SHA1 Message Date
Timothy Carambat d72f1af361
Improve uploader experience (#1205)
* Improve uploader expierence
- Wipe upload container (fadeout) after uploading
- debounce fetchKeys by 1s

* patch unneded exports
2024-04-26 17:41:42 -07:00
Sean Hatfield 360f17cd58
[FIX] Move to Workspace popup UI bug fix (#1204)
fix for popup menu transparent container
2024-04-26 17:38:41 -07:00
Sean Hatfield 8eda75d624
[FIX] Loading message in document picker bug (#1202)
* fix loading message in document picker bug

* linting

---------

Co-authored-by: timothycarambat <rambat1010@gmail.com>
2024-04-26 17:08:10 -07:00
Timothy Carambat 1b35bcbeab
Strengthen field validations on user Updates (#1201)
* Strengthen field validations on user Updates

* update writables
2024-04-26 16:46:04 -07:00
timothycarambat df2c01b176 patch OpenRouter model fetcher when key is not present 2024-04-26 15:58:30 -07:00
8 changed files with 227 additions and 63 deletions

View File

@ -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>

View File

@ -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">

View File

@ -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>

View File

@ -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;
}
}

View File

@ -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 });

View File

@ -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,
};

View File

@ -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,
});

View File

@ -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 };