feat: add loading card for memories page

This commit is contained in:
Kush Thaker 2024-09-10 13:20:00 +05:30
parent 1dafbc691b
commit e05c644143
23 changed files with 1485 additions and 2218 deletions

View file

@ -25,7 +25,7 @@ import chunkText from "./queueConsumer/chunkers/chonker";
import { systemPrompt, template } from "./prompts/prompt1";
import { swaggerUI } from "@hono/swagger-ui";
import { database } from "./db";
import { storedContent } from "@repo/db/schema";
import { jobs, storedContent, users } from "@repo/db/schema";
import { sql, and, eq } from "drizzle-orm";
import { LIMITS } from "@repo/shared-types";
import { typeDecider } from "./queueConsumer/utils/typeDecider";
@ -34,8 +34,8 @@ import {
chunkNote,
chunkPage,
} from "./queueConsumer/chunkers/chunkPageOrNotes";
import { queue } from "./queueConsumer";
import { isErr } from "./errors/results";
import { d1ErrorFactory, queue } from "./queueConsumer";
import { isErr, wrap } from "./errors/results";
import { createOpenAI } from "@ai-sdk/openai";
const app = new Hono<{ Bindings: Env }>();
@ -119,11 +119,33 @@ app.post("/api/add", zValidator("json", vectorBody), async (c) => {
// unique contraint check
if (isWithinLimit) {
//insert to the jobs table
const job = await wrap(
db
.insert(jobs)
.values({
userId: body.user,
url: saveToDbUrl,
status: "Processing",
attempts: 0,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning({ jobId: jobs.id }),
d1ErrorFactory,
"Error When inserting into jobs table",
);
if (isErr(job)) {
throw job.error;
}
const jobId = job.value[0].jobId;
await c.env.EMBEDCHUNKS_QUEUE.send({
content: saveToDbUrl,
user: body.user,
space: body.spaces,
type: type,
jobId: jobId,
});
} else {
return c.json({
@ -775,6 +797,31 @@ app.get("/howFuckedAreWe", async (c) => {
return c.json({ fuckedPercent });
});
app.get(
"api/memories",
zValidator(
"query",
z.object({
userId: z.string(),
}),
),
async (c) => {
const { userId } = c.req.valid("query");
const db = database(c.env);
const spaces = await db.query.space.findMany({
where: eq(users, userId),
});
const memories = await db.query.storedContent.findMany({
where: eq(users, userId),
});
const processingJobs = await db.query.jobs.findMany({
where: and(eq(jobs.userId, userId), eq(jobs.status, "Processing")),
});
return c.json({ spaces, memories, processingJobs });
},
);
export default {
fetch: app.fetch,
queue,

View file

@ -33,7 +33,6 @@ export function chunkThread(threadText: string): TweetChunks {
if (typeof threadText == "string") {
console.log("DA WORKER FAILED DO SOMEHTING FIX DA WROKER", thread);
const rawTweet = getRawTweet(thread);
console.log(rawTweet);
const parsedTweet: any = JSON.parse(rawTweet);
const chunkedTweet = chunkText(parsedTweet.text, 1536);
@ -56,7 +55,6 @@ export function chunkThread(threadText: string): TweetChunks {
return { type: "tweet", chunks };
} else {
console.log("thread in else statement", JSON.stringify(thread));
const chunkedTweets = (thread as any).map((tweet: Tweet) => {
const chunkedTweet = chunkText(tweet.text, 1536);

View file

@ -34,7 +34,6 @@ export async function processPage(input: {
throw metadataResult.error;
}
const metadata = metadataResult.value;
console.log("[this is the metadata]", metadata);
return Ok({ pageContent, metadata });
} catch (e) {
console.error("[Page Processing Error]", e);

View file

@ -17,7 +17,6 @@ export const getTweetData = async (
tweetID: string,
): Promise<Result<GetTweetResult, ProcessTweetError>> => {
try {
console.log("is fetch defined here?");
const url = `https://cdn.syndication.twimg.com/tweet-result?id=${tweetID}&lang=en&features=tfw_timeline_list%3A%3Btfw_follower_count_sunset%3Atrue%3Btfw_tweet_edit_backend%3Aon%3Btfw_refsrc_session%3Aon%3Btfw_fosnr_soft_interventions_enabled%3Aon%3Btfw_show_birdwatch_pivots_enabled%3Aon%3Btfw_show_business_verified_badge%3Aon%3Btfw_duplicate_scribes_to_settings%3Aon%3Btfw_use_profile_image_shape_enabled%3Aon%3Btfw_show_blue_verified_badge%3Aon%3Btfw_legacy_timeline_sunset%3Atrue%3Btfw_show_gov_verified_badge%3Aon%3Btfw_show_business_affiliate_badge%3Aon%3Btfw_tweet_edit_frontend%3Aon&token=4c2mmul6mnh`;
const resp = await fetch(url, {

View file

@ -35,7 +35,7 @@ class D1InsertError extends BaseError {
}
}
const d1ErrorFactory = (err: Error, source: string) =>
export const d1ErrorFactory = (err: Error, source: string) =>
new D1InsertError(err.message, source);
const calculateExponentialBackoff = (
@ -52,11 +52,12 @@ export async function queue(
space: Array<number>;
user: string;
type: string;
jobId: number;
}>,
env: Env,
): Promise<void> {
const db = database(env);
console.log("[This is the batch]", JSON.stringify(batch));
for (let message of batch.messages) {
const body = message.body;
@ -71,63 +72,38 @@ export async function queue(
throw userExists.error;
}
//check if this is a retry job.. by checking if the combination of the userId and the url already exists on the queue
let jobId;
const existingJob = await wrap(
db
.select()
.from(jobs)
.where(
and(
eq(jobs.userId, userExists.value[0].id),
eq(jobs.url, body.content),
),
)
.limit(1),
//take the job id from the body and update the job table with attempts + 1
const jobId = body.jobId;
const jobResult = await wrap(
db.select().from(jobs).where(eq(jobs.id, jobId)).limit(1),
d1ErrorFactory,
"Error when checking for existing job",
"Error when fetching job",
);
if (isErr(existingJob)) {
throw existingJob.error;
if (isErr(jobResult)) {
throw jobResult.error;
}
if (existingJob.value.length > 0) {
jobId = existingJob.value[0].id;
await wrap(
db
.update(jobs)
.set({
attempts: existingJob.value[0].attempts + 1,
updatedAt: new Date(),
status: "Processing",
})
.where(eq(jobs.id, jobId)),
d1ErrorFactory,
"Error when updating job attempts",
);
} else {
const job = await wrap(
db
.insert(jobs)
.values({
userId: userExists.value[0].id as string,
url: body.content,
status: "Processing",
attempts: 1,
createdAt: new Date(),
updatedAt: new Date(),
})
.returning({ jobId: jobs.id }),
d1ErrorFactory,
"Error When inserting into jobs table",
);
if (isErr(job)) {
throw job.error;
}
jobId = job.value[0].jobId;
const job = jobResult.value[0];
if (!job) {
throw new Error("Job not found");
}
await wrap(
db
.update(jobs)
.set({
attempts: job.attempts + 1,
updatedAt: new Date(),
status: "Processing",
})
.where(eq(jobs.id, jobId)),
d1ErrorFactory,
"Error when updating job attempts",
);
let pageContent: string;
let vectorData: string;
let metadata: Metadata;
@ -136,7 +112,6 @@ export async function queue(
let noteId = 0;
switch (type) {
case "note": {
console.log("note hit");
const note = processNote(body.content);
if (isErr(note)) {
throw note.error;
@ -149,13 +124,11 @@ export async function queue(
break;
}
case "page": {
console.log("page hit");
const page = await processPage({
url: body.content,
securityKey: env.MD_SEC_KEY,
});
if (isErr(page)) {
console.log("there is a page error here");
throw page.error;
}
pageContent = page.value.pageContent;
@ -172,7 +145,7 @@ export async function queue(
tweetUrl: body.content,
env: env,
});
console.log("[This is the thread]", thread);
if (isErr(tweet)) {
throw tweet.error;
}
@ -368,9 +341,8 @@ export async function queue(
.update(jobs)
.set({ status: "Processed" })
.where(eq(jobs.id, jobId));
return;
}
console.log(`Finished processing batch of ${batch.messages.length} messages`);
}
/*

View file

@ -30,6 +30,7 @@ export interface JobData {
space: Array<number>;
user: string;
type: string;
jobId: number;
}
export interface TweetData {

View file

@ -1,6 +1,6 @@
"use client";
import { Content, StoredSpace } from "@repo/db/schema";
import { Content, Job, jobs, StoredSpace } from "@repo/db/schema";
import { MemoriesIcon, NextIcon, UrlIcon } from "@repo/ui/icons";
import {
ArrowLeftIcon,
@ -12,10 +12,11 @@ import {
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import React, { useMemo, useState } from "react";
import React, { useEffect, useMemo, useState } from "react";
import Masonry from "react-layout-masonry";
import { getRawTweet } from "@repo/shared-types/utils";
import { MyTweet } from "../../../components/twitter/render-tweet";
import LoadingCard from "@/components/loadingCard/loadingCard";
import {
DropdownMenu,
DropdownMenuContent,
@ -53,14 +54,25 @@ import {
DocumentTextIcon,
FolderIcon,
} from "@heroicons/react/24/outline";
import { db } from "@/server/db";
import { and, count, eq } from "drizzle-orm";
import { usePendingJob } from "../../../contexts/PendingJobContext";
type TMemoriesPage = {
memoriesAndSpaces: { memories: Content[]; spaces: StoredSpace[] };
memoriesAndSpaces: {
memories: Content[];
spaces: StoredSpace[];
userId: string;
};
title?: string;
currentSpace?: StoredSpace;
usersWithAccess?: string[];
};
interface JobCountResponse {
pendingJobs: number;
}
export function MemoriesPage({
memoriesAndSpaces,
title = "Your Memories",
@ -78,8 +90,65 @@ export function MemoriesPage({
return "All";
}, [tab]);
const userId = memoriesAndSpaces.userId;
const [filter, setFilter] = useState(initialFilter);
const [spaces, setSpaces] = useState<StoredSpace[]>(memoriesAndSpaces.spaces);
const [newMemories, setNewMemories] = useState<Content[]>([]);
const [lastFetchTimestamp, setLastFetchTimestamp] = useState<string>(
new Date().toISOString(),
);
const { pendingJobs, setPendingJobs } = usePendingJob();
const fetchNewMemories = async (lastFetchTimestamp: string) => {
try {
const response = await fetch(
"/api/getNewMemories?lastFetchTimestamp=" + lastFetchTimestamp,
);
if (!response.ok) {
throw new Error("Failed to fetch new memories");
}
const data = (await response.json()) as { newMemories: Content[] };
return data.newMemories;
} catch (error) {
console.error("Error fetching new memories:", error);
}
};
useEffect(() => {
const fetchJobCount = async () => {
try {
const response = await fetch("/api/getJobCount");
if (!response.ok) {
throw new Error("Failed to fetch job count");
}
const data: JobCountResponse = await response.json();
const newPendingJobs = data.pendingJobs;
// If pendingJobs has decreased, fetch new memories with the lastFetchTimestamp
if (newPendingJobs < pendingJobs) {
const newMemories = await fetchNewMemories(lastFetchTimestamp);
if (newMemories) {
setNewMemories((prev) => [...prev, ...newMemories]);
setLastFetchTimestamp(new Date().toISOString());
}
}
setPendingJobs(newPendingJobs);
} catch (error) {
console.error("Error fetching job count:", error);
}
};
fetchJobCount(); // Initial fetch
const intervalId = setInterval(fetchJobCount, 5000); // Poll every 5 seconds
return () => clearInterval(intervalId); // Clean up on unmount
}, [
pendingJobs,
memoriesAndSpaces.memories,
setPendingJobs,
lastFetchTimestamp,
]);
const handleDeleteSpace = async (id: number) => {
const response = await deleteSpace(id);
@ -102,7 +171,6 @@ export function MemoriesPage({
const json = JSON.stringify(dataToExport, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "memories_and_spaces.json";
@ -115,6 +183,11 @@ export function MemoriesPage({
const sortedItems = useMemo(() => {
// merge spaces & memories to { item: "memory" | "space", date: Date, data: Content | StoredSpace }
const unifiedItems = [
...newMemories.map((memory) => ({
item: "memory",
date: new Date(memory.savedAt),
data: memory,
})),
...memoriesAndSpaces.memories.map((memory) => ({
item: "memory",
date: new Date(memory.savedAt),
@ -152,18 +225,19 @@ export function MemoriesPage({
return false;
})
.sort((a, b) => b.date - a.date);
}, [memoriesAndSpaces.memories, spaces, filter]);
}, [memoriesAndSpaces.memories, spaces, filter, newMemories]);
return (
<div className="px-2 md:px-32 py-36 h-full flex mx-auto w-full flex-col gap-6">
<div className="space-y-2">
{currentSpace && (
<Link href={"/memories"}>
<Button className="px-0 text-gray-300" variant="link">
<Button className="px-0 text-gray-300" variant="link">
<ArrowLeftIcon className="w-3 h-3" /> Back to all memories
</Button>
</Link>
)}
<h2 className="text-white w-full text-3xl text-left font-semibold">
{title}
</h2>
@ -177,6 +251,7 @@ export function MemoriesPage({
currentSpace ? SpaceFilterMethods : MemoriesFilterMethods
}
/>
<button
onClick={handleExport}
className={`transition px-4 py-2 rounded-lg text-[#B3BCC5] bg-secondary hover:bg-secondary hover:text-[#76a3cc]`}
@ -198,6 +273,7 @@ export function MemoriesPage({
alt="Spaces icon"
className="w-3 h-3"
/>
<span className="text-[#fff]">{user}</span>
</div>
))}
@ -211,6 +287,7 @@ export function MemoriesPage({
if (!email) {
toast.error("Please enter an email");
return;
}
@ -229,6 +306,7 @@ export function MemoriesPage({
name="email"
placeholder="Add user by email"
/>
<Button variant="secondary">Add</Button>
</form>
</div>
@ -242,10 +320,17 @@ export function MemoriesPage({
className: "min-w-[calc(33.3333%-16px)] w-full",
}}
>
{pendingJobs > 0 && (
<div className="w-full">
<LoadingCard pendingJobs={pendingJobs} />
</div>
)}
{sortedItems.map((item) => {
if (item.item === "memory") {
return (
<MemoryComponent
key={item.data.id}
type={(item.data as Content).type ?? "note"}
content={(item.data as Content).content}
title={(item.data as Content).title ?? "Untitled"}
@ -267,6 +352,7 @@ export function MemoriesPage({
if (item.item === "space") {
return (
<SpaceComponent
key={item.data.id}
title={(item.data as StoredSpace).name}
description={`${(item.data as StoredSpace).numItems} memories`}
id={(item.data as StoredSpace).id}
@ -318,7 +404,7 @@ function SpaceComponent({
<Image src={NextIcon} alt="Search icon" />
</div>
</Link>
<div className="absolute z-40 right-3 top-3 opacity-0 group-hover:opacity-100 hover:text-red-600">
<div className="absolute z-40 right-5 top-4 opacity-0 group-hover:opacity-100 hover:text-red-600">
<SpaceDeleteAlert
onClick={() => {
handleDeleteSpace(id);
@ -446,7 +532,7 @@ function MemoryComponent({
}
const MemoriesFilterMethods = ["All", "Spaces", "Pages", "Notes", "Tweet"];
const SpaceFilterMethods = ["All", "Pages", "Notes", "Tweet"];
const SpaceFilterMethods = ["All", "Pages", "Notes", "Tweet", "Jobs"];
function Filters({
setFilter,
filter,
@ -463,7 +549,7 @@ function Filters({
<div className="relative">
<button
onClick={() => setFilter(i)}
className={`transition px-4 py-2 ${i === filter ? " text-[#369DFD]" : "text-[#B3BCC5]"}`}
className={`transition px-4 py-2 ${i === filter ? " text-[#369DFD]" : "text-[#B3BCC5]"}`}
>
{i}
</button>

View file

@ -18,7 +18,7 @@ async function Page({ params: { spaceid } }: { params: { spaceid: number } }) {
return (
<MemoriesPage
memoriesAndSpaces={{ memories: data.memories, spaces: [] }}
memoriesAndSpaces={{ memories: data.memories, spaces: [], userId: "" }} //TODO: Fix this please
title={data.spaces[0]?.name}
currentSpace={data.spaces[0]}
usersWithAccess={hasAccess.map((x) => x.userEmail) ?? []}

View file

@ -31,6 +31,7 @@ import ComboboxWithCreate from "@repo/ui/shadcn/combobox";
import { StoredSpace } from "@repo/db/schema";
import { useKeyPress } from "@/lib/useKeyPress";
import { useFormStatus } from "react-dom";
import { usePendingJob } from "@/contexts/PendingJobContext";
function Menu() {
useKeyPress("a", () => {
@ -218,7 +219,7 @@ function DialogContentMenu({ setDialogClose }: { setDialogClose: () => void }) {
setSelectedSpaces([]);
return cont;
};
const { pendingJobs, setPendingJobs } = usePendingJob();
const formSubmit = () => {
toast.promise(handleSubmit(content, selectedSpaces), {
loading: (
@ -227,8 +228,14 @@ function DialogContentMenu({ setDialogClose }: { setDialogClose: () => void }) {
Creating memory...
</span>
),
success: (data) => "Memory queued",
error: (error) => `Memory creation failed: ${error}`,
success: (data) => {
setPendingJobs(pendingJobs + 1);
return "Memory queued";
},
error: (error) => {
setPendingJobs(pendingJobs - 1);
return `Memory creation failed: ${error}`;
},
richColors: true,
});
};

View file

@ -218,250 +218,6 @@ export const createMemory = async (input: {
};
}
// const type = typeDecider(input.content);
// let pageContent = input.content;
// let metadata: Awaited<ReturnType<typeof getMetaData>>;
// let vectorData: string;
// if (!(await limit(data.user.id, type))) {
// return {
// success: false,
// data: 0,
// error: `You have exceeded the limit of ${LIMITS[type as keyof typeof LIMITS]} ${type}s.`,
// };
// } --> How would this fit in the backend???
// let noteId = 0;
// if (type === "page") {
// const response = await fetch("https://md.dhr.wtf/?url=" + input.content, {
// headers: {
// Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
// },
// });
// pageContent = await response.text();
// vectorData = pageContent;
// try {
// metadata = await getMetaData(input.content);
// } catch (e) {
// return {
// success: false,
// error: "Failed to fetch metadata for the page. Please try again later.",
// };
// }
// } else if (type === "tweet") {
// //Request the worker for the entire thread
// let thread: string;
// let errorOccurred: boolean = false;
// try {
// const cf_thread_endpoint = process.env.THREAD_CF_WORKER;
// const authKey = process.env.THREAD_CF_AUTH;
// const threadRequest = await fetch(cf_thread_endpoint, {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// Authorization: authKey,
// },
// body: JSON.stringify({ url: input.content }),
// });
// if (threadRequest.status !== 200) {
// throw new Error(
// `Failed to fetch the thread: ${input.content}, Reason: ${threadRequest.statusText}`,
// );
// }
// thread = await threadRequest.text();
// if (thread.trim().length === 2) {
// console.log("Thread is an empty array");
// throw new Error(
// "[THREAD FETCHING SERVICE] Got no content form thread worker",
// );
// }
// } catch (e) {
// console.log("[THREAD FETCHING SERVICE] Failed to fetch the thread", e);
// errorOccurred = true;
// }
// const tweet = await getTweetData(input.content.split("/").pop() as string);
// pageContent = tweetToMd(tweet);
// console.log("THis ishte page content!!", pageContent);
// //@ts-ignore
// vectorData = errorOccurred ? JSON.stringify(pageContent) : thread;
// metadata = {
// baseUrl: input.content,
// description: tweet.text.slice(0, 200),
// image: tweet.user.profile_image_url_https,
// title: `Tweet by ${tweet.user.name}`,
// };
// } else if (type === "note") {
// pageContent = input.content;
// vectorData = pageContent;
// noteId = new Date().getTime();
// metadata = {
// baseUrl: `https://supermemory.ai/note/${noteId}`,
// description: `Note created at ${new Date().toLocaleString()}`,
// image: "https://supermemory.ai/logo.png",
// title: `${pageContent.slice(0, 20)} ${pageContent.length > 20 ? "..." : ""}`,
// };
// } else {
// return {
// success: false,
// data: 0,
// error: "Invalid type",
// };
// }
// let storeToSpaces = input.spaces;
// if (!storeToSpaces) {
// storeToSpaces = [];
// }
// const vectorSaveResponse = await fetch(
// `${process.env.BACKEND_BASE_URL}/api/add`,
// {
// method: "POST",
// body: JSON.stringify({
// pageContent: vectorData,
// title: metadata.title,
// description: metadata.description,
// url: metadata.baseUrl,
// spaces: storeToSpaces.map((spaceId) => spaceId.toString()),
// user: data.user.id,
// type,
// }),
// headers: {
// "Content-Type": "application/json",
// Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
// },
// },
// );
// if (!vectorSaveResponse.ok) {
// const errorData = await vectorSaveResponse.text();
// console.error(errorData);
// return {
// success: false,
// data: 0,
// error: `Failed to save to vector store. Backend returned error: ${errorData}`,
// };
// }
// let contentId: number;
// const response = (await vectorSaveResponse.json()) as {
// status: string;
// chunkedInput: string;
// message?: string;
// };
// try {
// if (response.status !== "ok") {
// if (response.status === "error") {
// return {
// success: false,
// data: 0,
// error: response.message,
// };
// } else {
// return {
// success: false,
// data: 0,
// error: `Failed to save to vector store. Backend returned error: ${response.message}`,
// };
// }
// }
// } catch (e) {
// return {
// success: false,
// data: 0,
// error: `Failed to save to vector store. Backend returned error: ${e}`,
// };
// }
// const saveToDbUrl =
// (metadata.baseUrl.split("#supermemory-user-")[0] ?? metadata.baseUrl) +
// "#supermemory-user-" +
// data.user.id;
// // Insert into database
// try {
// const insertResponse = await db
// .insert(storedContent)
// .values({
// content: pageContent,
// title: metadata.title,
// description: metadata.description,
// url: saveToDbUrl,
// baseUrl: saveToDbUrl,
// image: metadata.image,
// savedAt: new Date(),
// userId: data.user.id,
// type,
// noteId,
// })
// .returning({ id: storedContent.id });
// revalidatePath("/memories");
// revalidatePath("/home");
// if (!insertResponse[0]?.id) {
// return {
// success: false,
// data: 0,
// error: "Something went wrong while saving the document to the database",
// };
// }
// contentId = insertResponse[0]?.id;
// } catch (e) {
// const error = e as Error;
// console.log("Error: ", error.message);
// if (
// error.message.includes(
// "D1_ERROR: UNIQUE constraint failed: storedContent.baseUrl",
// )
// ) {
// return {
// success: false,
// data: 0,
// error: "Content already exists",
// };
// }
// return {
// success: false,
// data: 0,
// error: "Failed to save to database with error: " + error.message,
// };
// }
// if (storeToSpaces.length > 0) {
// // Adding the many-to-many relationship between content and spaces
// const spaceData = await db
// .select()
// .from(space)
// .where(
// and(inArray(space.id, storeToSpaces), eq(space.user, data.user.id)),
// )
// .all();
// await Promise.all(
// spaceData.map(async (s) => {
// await db
// .insert(contentToSpace)
// .values({ contentId: contentId, spaceId: s.id });
// await db.update(space).set({ numItems: s.numItems + 1 });
// }),
// );
// }
return {
success: true,
data: 1,

View file

@ -9,6 +9,7 @@ import {
chatThreads,
Content,
contentToSpace,
Job,
space,
spacesAccess,
storedContent,
@ -158,10 +159,10 @@ export const getMemoriesInsideSpace = async (
},
};
};
export const getAllUserMemoriesAndSpaces = async (): ServerActionReturnType<{
spaces: StoredSpace[];
memories: Content[];
userId: string;
}> => {
const data = await auth();

View file

@ -0,0 +1,33 @@
import { db } from "@/server/db";
import { and, eq, sql } from "drizzle-orm";
import { jobs } from "@repo/db/schema";
import { type NextRequest, NextResponse } from "next/server";
import { ensureAuth } from "../ensureAuth";
export const runtime = "edge";
export async function GET(req: NextRequest) {
const session = await ensureAuth(req);
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
// Calculate the timestamp for one hour ago
const oneHourAgoTimestamp = Math.floor(Date.now() / 1000) - 3600;
const jobCount = await db
.select({ count: sql<number>`count(*)`.mapWith(Number) })
.from(jobs)
.where(
and(
eq(jobs.userId, session.user.id),
eq(jobs.status, "Processing"),
sql`${jobs.updatedAt} > ${oneHourAgoTimestamp}`,
),
);
return NextResponse.json({
pendingJobs: jobCount[0]?.count ?? 0,
});
}

View file

@ -0,0 +1,46 @@
import { db } from "@/server/db";
import { storedContent } from "@repo/db/schema";
import { eq, gt } from "drizzle-orm";
import { ensureAuth } from "../ensureAuth";
import { type NextRequest, NextResponse } from "next/server";
export const runtime = "edge";
export async function GET(req: NextRequest) {
const session = await ensureAuth(req);
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
const lastFetchTimestamp = req.nextUrl.searchParams.get("lastFetchTimestamp");
if (!lastFetchTimestamp) {
return NextResponse.json(
{ error: "Missing required parameters" },
{ status: 400 },
);
}
if (!lastFetchTimestamp) {
return NextResponse.json(
{ error: "Missing required parameters" },
{ status: 400 },
);
}
try {
const newMemories = await db.query.storedContent.findMany({
where: (content) =>
eq(content.userId, session.user.id) &&
gt(content.savedAt, new Date(lastFetchTimestamp)),
});
return NextResponse.json({ newMemories });
} catch (error) {
console.error("Error fetching new memories:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 },
);
}
}

View file

@ -0,0 +1,52 @@
import { NextRequest } from "next/server";
import { z } from "zod";
import { ensureAuth } from "../../ensureAuth";
export const runtime = "edge";
const vectorBody = z.object({
spaces: z.array(z.number()).optional(),
url: z.string(),
});
export async function POST(req: NextRequest) {
const session = await ensureAuth(req);
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
try {
const body = await req.json();
const validatedBody = vectorBody.parse(body);
const vectorSaveResponses = await fetch(
`${process.env.BACKEND_BASE_URL}/api/add`,
{
method: "POST",
body: JSON.stringify({
url: validatedBody.url,
spaces: validatedBody.spaces,
user: session.user.id,
}),
headers: {
"Content-Type": "application/json",
Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
},
},
);
const response = (await vectorSaveResponses.json()) as {
status: string;
message?: string;
};
if (response.status !== "ok") {
return new Response("Internal server error", { status: 500 });
}
if (response.status === "ok") {
return new Response("Added to queue", { status: 200 });
}
} catch (e) {
return new Response("Bad Request", { status: 400 });
}
}

View file

@ -6,7 +6,7 @@ import { GeistSans } from "geist/font/sans";
import { GeistMono } from "geist/font/mono";
import { cn } from "@repo/ui/lib/utils";
import { Toaster } from "@repo/ui/shadcn/toaster";
import { PHProvider } from "./providers";
import { PendingJobsProvider, PHProvider } from "./providers";
import PostHogPageView from "./PHPageViews";
const inter = Inter({ subsets: ["latin"] });
@ -78,17 +78,19 @@ export default function RootLayout({
></script>
</head>
<PHProvider>
<body
className={cn(
`${inter.className} dark`,
GeistMono.variable,
GeistSans.variable,
)}
>
{children}
<Toaster />
{/* <PostHogPageView /> */}
</body>
<PendingJobsProvider>
<body
className={cn(
`${inter.className} dark`,
GeistMono.variable,
GeistSans.variable,
)}
>
{children}
<Toaster />
{/* <PostHogPageView /> */}
</body>
</PendingJobsProvider>
</PHProvider>
</html>
);

View file

@ -1,5 +1,6 @@
// app/providers.tsx
"use client";
import { PendingJobProvider } from "@/contexts/PendingJobContext";
import posthog from "posthog-js";
import { PostHogProvider } from "posthog-js/react";
@ -14,3 +15,11 @@ if (typeof window !== "undefined") {
export function PHProvider({ children }: { children: React.ReactNode }) {
return <PostHogProvider client={posthog}>{children}</PostHogProvider>;
}
export function PendingJobsProvider({
children,
}: {
children: React.ReactNode;
}) {
return <PendingJobProvider>{children}</PendingJobProvider>;
}

View file

@ -0,0 +1,62 @@
import { motion } from "framer-motion";
const LoadingCard = ({ pendingJobs }: { pendingJobs: number }) => {
if (pendingJobs === 0) {
return null;
}
return (
<motion.div
className="relative bg-[#2D2D2D] rounded-xl overflow-hidden w-full min-h-[100px] max-h-[200px]"
animate={{
boxShadow: [
"0 0 0 2px rgba(186, 66, 255, 0.5)",
"0 0 0 2px rgba(0, 225, 255, 0.8)",
"0 0 0 2px rgba(186, 66, 255, 0.5)",
],
}}
transition={{
duration: 3,
repeat: Infinity,
ease: "linear",
}}
>
<div className="absolute inset-0.5 bg-[#2D2D2D] rounded-xl z-10"></div>
<div className="relative z-20 flex flex-col items-center justify-center h-full text-white p-4">
<motion.div
className="w-12 h-12 mb-2 rounded-full relative"
style={{
backgroundImage:
"linear-gradient(rgb(186, 66, 255) 35%, rgb(0, 225, 255))",
filter: "blur(1px)",
boxShadow:
"0px -4px 16px 0px rgb(186, 66, 255), 0px 4px 16px 0px rgb(0, 225, 255)",
}}
animate={{ rotate: 360 }}
transition={{
duration: 1.7,
repeat: Infinity,
ease: "linear",
}}
>
<div
className="absolute inset-0 rounded-full"
style={{
backgroundColor: "rgb(36, 36, 36)",
filter: "blur(8px)",
}}
></div>
</motion.div>
<p className="text-base font-semibold text-center">One moment</p>
<p className="text-xs text-gray-400 text-center">
Saving {pendingJobs} {pendingJobs === 1 ? "memory" : "memories"} for
you
</p>
</div>
</motion.div>
);
};
export default LoadingCard;

View file

@ -0,0 +1,31 @@
"use client";
import React, { createContext, useState, useContext } from "react";
interface PendingJobContextType {
pendingJobs: number;
setPendingJobs: (pendingJobs: number) => void;
}
const PendingJobContext = createContext<PendingJobContextType | undefined>(
undefined,
);
export const PendingJobProvider: React.FC<{ children: React.ReactNode }> = ({
children,
}) => {
const [pendingJobs, setPendingJobs] = useState(0);
return (
<PendingJobContext.Provider value={{ pendingJobs, setPendingJobs }}>
{children}
</PendingJobContext.Provider>
);
};
export const usePendingJob = () => {
const context = useContext(PendingJobContext);
if (context === undefined) {
throw new Error("usePendingJob must be used within a PendingJobProvider");
}
return context;
};

View file

@ -43,7 +43,7 @@ CREATE TABLE `chatHistory` (
`answerParts` text,
`answerSources` text,
`answerJustification` text,
`createdAt` integer DEFAULT '"2024-07-31T07:35:53.819Z"' NOT NULL,
`createdAt` integer DEFAULT '"2024-09-05T18:04:25.816Z"' NOT NULL,
FOREIGN KEY (`threadId`) REFERENCES `chatThread`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
@ -70,8 +70,8 @@ CREATE TABLE `jobs` (
`attempts` integer DEFAULT 0 NOT NULL,
`lastAttemptAt` integer,
`error` blob,
`createdAt` integer NOT NULL,
`updatedAt` integer NOT NULL,
`createdAt` integer DEFAULT '"2024-09-05T18:04:25.816Z"' NOT NULL,
`updatedAt` integer DEFAULT '"2024-09-05T18:04:25.816Z"' NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
@ -105,8 +105,8 @@ CREATE TABLE `storedContent` (
`description` text(255),
`url` text NOT NULL,
`savedAt` integer NOT NULL,
`baseUrl` text(255),
`ogImage` text(255),
`baseUrl` text,
`ogImage` text,
`type` text DEFAULT 'page',
`image` text(255),
`user` text,
@ -142,7 +142,6 @@ CREATE INDEX `jobs_url_idx` ON `jobs` (`url`);--> statement-breakpoint
CREATE UNIQUE INDEX `space_name_unique` ON `space` (`name`);--> statement-breakpoint
CREATE INDEX `spaces_name_idx` ON `space` (`name`);--> statement-breakpoint
CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint
CREATE UNIQUE INDEX `storedContent_baseUrl_unique` ON `storedContent` (`baseUrl`);--> statement-breakpoint
CREATE INDEX `storedContent_url_idx` ON `storedContent` (`url`);--> statement-breakpoint
CREATE INDEX `storedContent_savedAt_idx` ON `storedContent` (`savedAt`);--> statement-breakpoint
CREATE INDEX `storedContent_title_idx` ON `storedContent` (`title`);--> statement-breakpoint

File diff suppressed because it is too large Load diff

View file

@ -1,923 +0,0 @@
{
"version": "6",
"dialect": "sqlite",
"id": "30bdc8fa-70a7-44b7-a06b-59b950a62c11",
"prevId": "3fbdb153-2764-4b09-ac22-05c3a131ec35",
"tables": {
"account": {
"name": "account",
"columns": {
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"provider": {
"name": "provider",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"refresh_token": {
"name": "refresh_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"access_token": {
"name": "access_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"expires_at": {
"name": "expires_at",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"token_type": {
"name": "token_type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"scope": {
"name": "scope",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"id_token": {
"name": "id_token",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"session_state": {
"name": "session_state",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"account_userId_user_id_fk": {
"name": "account_userId_user_id_fk",
"tableFrom": "account",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"account_provider_providerAccountId_pk": {
"columns": ["provider", "providerAccountId"],
"name": "account_provider_providerAccountId_pk"
}
},
"uniqueConstraints": {}
},
"authenticator": {
"name": "authenticator",
"columns": {
"credentialID": {
"name": "credentialID",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"providerAccountId": {
"name": "providerAccountId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentialPublicKey": {
"name": "credentialPublicKey",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"counter": {
"name": "counter",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentialDeviceType": {
"name": "credentialDeviceType",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"credentialBackedUp": {
"name": "credentialBackedUp",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"transports": {
"name": "transports",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"authenticator_credentialID_unique": {
"name": "authenticator_credentialID_unique",
"columns": ["credentialID"],
"isUnique": true
}
},
"foreignKeys": {
"authenticator_userId_user_id_fk": {
"name": "authenticator_userId_user_id_fk",
"tableFrom": "authenticator",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"authenticator_userId_credentialID_pk": {
"columns": ["credentialID", "userId"],
"name": "authenticator_userId_credentialID_pk"
}
},
"uniqueConstraints": {}
},
"canvas": {
"name": "canvas",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Untitled'"
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Untitled'"
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"canvas_user_userId": {
"name": "canvas_user_userId",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"canvas_userId_user_id_fk": {
"name": "canvas_userId_user_id_fk",
"tableFrom": "canvas",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"chatHistory": {
"name": "chatHistory",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"threadId": {
"name": "threadId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"question": {
"name": "question",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"answerParts": {
"name": "answerParts",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"answerSources": {
"name": "answerSources",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"answerJustification": {
"name": "answerJustification",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2024-08-27T18:12:22.062Z\"'"
}
},
"indexes": {
"chatHistory_thread_idx": {
"name": "chatHistory_thread_idx",
"columns": ["threadId"],
"isUnique": false
}
},
"foreignKeys": {
"chatHistory_threadId_chatThread_id_fk": {
"name": "chatHistory_threadId_chatThread_id_fk",
"tableFrom": "chatHistory",
"tableTo": "chatThread",
"columnsFrom": ["threadId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"chatThread": {
"name": "chatThread",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"firstMessage": {
"name": "firstMessage",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"chatThread_user_idx": {
"name": "chatThread_user_idx",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"chatThread_userId_user_id_fk": {
"name": "chatThread_userId_user_id_fk",
"tableFrom": "chatThread",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"contentToSpace": {
"name": "contentToSpace",
"columns": {
"contentId": {
"name": "contentId",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"spaceId": {
"name": "spaceId",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"contentToSpace_contentId_storedContent_id_fk": {
"name": "contentToSpace_contentId_storedContent_id_fk",
"tableFrom": "contentToSpace",
"tableTo": "storedContent",
"columnsFrom": ["contentId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
},
"contentToSpace_spaceId_space_id_fk": {
"name": "contentToSpace_spaceId_space_id_fk",
"tableFrom": "contentToSpace",
"tableTo": "space",
"columnsFrom": ["spaceId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"contentToSpace_contentId_spaceId_pk": {
"columns": ["contentId", "spaceId"],
"name": "contentToSpace_contentId_spaceId_pk"
}
},
"uniqueConstraints": {}
},
"jobs": {
"name": "jobs",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"status": {
"name": "status",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"attempts": {
"name": "attempts",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
},
"lastAttemptAt": {
"name": "lastAttemptAt",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"error": {
"name": "error",
"type": "blob",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2024-08-27T18:12:22.062Z\"'"
},
"updatedAt": {
"name": "updatedAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'\"2024-08-27T18:12:22.062Z\"'"
}
},
"indexes": {
"jobs_userId_idx": {
"name": "jobs_userId_idx",
"columns": ["userId"],
"isUnique": false
},
"jobs_status_idx": {
"name": "jobs_status_idx",
"columns": ["status"],
"isUnique": false
},
"jobs_createdAt_idx": {
"name": "jobs_createdAt_idx",
"columns": ["createdAt"],
"isUnique": false
},
"jobs_url_idx": {
"name": "jobs_url_idx",
"columns": ["url"],
"isUnique": false
}
},
"foreignKeys": {
"jobs_userId_user_id_fk": {
"name": "jobs_userId_user_id_fk",
"tableFrom": "jobs",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"session": {
"name": "session",
"columns": {
"sessionToken": {
"name": "sessionToken",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"session_userId_user_id_fk": {
"name": "session_userId_user_id_fk",
"tableFrom": "session",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"space": {
"name": "space",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'none'"
},
"user": {
"name": "user",
"type": "text(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"numItems": {
"name": "numItems",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": 0
}
},
"indexes": {
"space_name_unique": {
"name": "space_name_unique",
"columns": ["name"],
"isUnique": true
},
"spaces_name_idx": {
"name": "spaces_name_idx",
"columns": ["name"],
"isUnique": false
},
"spaces_user_idx": {
"name": "spaces_user_idx",
"columns": ["user"],
"isUnique": false
}
},
"foreignKeys": {
"space_user_user_id_fk": {
"name": "space_user_user_id_fk",
"tableFrom": "space",
"tableTo": "user",
"columnsFrom": ["user"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"spacesAccess": {
"name": "spacesAccess",
"columns": {
"spaceId": {
"name": "spaceId",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"userEmail": {
"name": "userEmail",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {
"spacesAccess_spaceId_space_id_fk": {
"name": "spacesAccess_spaceId_space_id_fk",
"tableFrom": "spacesAccess",
"tableTo": "space",
"columnsFrom": ["spaceId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {
"spacesAccess_spaceId_userEmail_pk": {
"columns": ["spaceId", "userEmail"],
"name": "spacesAccess_spaceId_userEmail_pk"
}
},
"uniqueConstraints": {}
},
"storedContent": {
"name": "storedContent",
"columns": {
"id": {
"name": "id",
"type": "integer",
"primaryKey": true,
"notNull": true,
"autoincrement": true
},
"content": {
"name": "content",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"description": {
"name": "description",
"type": "text(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"savedAt": {
"name": "savedAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"baseUrl": {
"name": "baseUrl",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"ogImage": {
"name": "ogImage",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"type": {
"name": "type",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": "'page'"
},
"image": {
"name": "image",
"type": "text(255)",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"user": {
"name": "user",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"noteId": {
"name": "noteId",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
}
},
"indexes": {
"storedContent_url_idx": {
"name": "storedContent_url_idx",
"columns": ["url"],
"isUnique": false
},
"storedContent_savedAt_idx": {
"name": "storedContent_savedAt_idx",
"columns": ["savedAt"],
"isUnique": false
},
"storedContent_title_idx": {
"name": "storedContent_title_idx",
"columns": ["title"],
"isUnique": false
},
"storedContent_user_idx": {
"name": "storedContent_user_idx",
"columns": ["user"],
"isUnique": false
}
},
"foreignKeys": {
"storedContent_user_user_id_fk": {
"name": "storedContent_user_user_id_fk",
"tableFrom": "storedContent",
"tableTo": "user",
"columnsFrom": ["user"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"user": {
"name": "user",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"name": {
"name": "name",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"email": {
"name": "email",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"emailVerified": {
"name": "emailVerified",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"image": {
"name": "image",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"telegramId": {
"name": "telegramId",
"type": "text",
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"hasOnboarded": {
"name": "hasOnboarded",
"type": "integer",
"primaryKey": false,
"notNull": false,
"autoincrement": false,
"default": false
}
},
"indexes": {
"users_email_idx": {
"name": "users_email_idx",
"columns": ["email"],
"isUnique": false
},
"users_telegram_idx": {
"name": "users_telegram_idx",
"columns": ["telegramId"],
"isUnique": false
},
"users_id_idx": {
"name": "users_id_idx",
"columns": ["id"],
"isUnique": false
}
},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"verificationToken": {
"name": "verificationToken",
"columns": {
"identifier": {
"name": "identifier",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"token": {
"name": "token",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
},
"expires": {
"name": "expires",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {
"verificationToken_identifier_token_pk": {
"columns": ["identifier", "token"],
"name": "verificationToken_identifier_token_pk"
}
},
"uniqueConstraints": {}
}
},
"enums": {},
"_meta": {
"schemas": {},
"tables": {},
"columns": {}
}
}

View file

@ -1,20 +1,13 @@
{
"version": "6",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1722411353835,
"tag": "0000_fixed_pandemic",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1724782342070,
"tag": "0001_huge_puma",
"breakpoints": true
}
]
}
"version": "6",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1725559465821,
"tag": "0000_adorable_iron_man",
"breakpoints": true
}
]
}

View file

@ -1,6 +1,6 @@
function convertStringToFixedNumber(input: string): number {
let hash = 0;
for (let i = 0; i < input.length; i++) {
for (let i = 0; i < input?.length; i++) {
const char = input.charCodeAt(i);
hash = (hash * 31 + char) % 1000000007; // Hashing by a large prime number
}
@ -19,7 +19,7 @@ function random(seed: number) {
}
function shuffle(array: string[], seed: number) {
let m = array.length,
let m = array?.length,
t,
i;
@ -38,7 +38,7 @@ function shuffle(array: string[], seed: number) {
export const cipher = (text: string) => {
let returned_text = "";
for (let i = 0; i < text.length; i++) {
for (let i = 0; i < text?.length; i++) {
returned_text += shuffled[chars.indexOf(text[i]!)];
}
@ -49,19 +49,19 @@ export const decipher = (text: string) => {
let returned_text = "";
const index = Math.floor(
random(convertStringToFixedNumber(process.env.BACKEND_SECURITY_KEY)) *
(text.length / 2),
(text?.length / 2),
);
for (let i = 0; i < text.length; i++) {
for (let i = 0; i < text?.length; i++) {
returned_text += chars[shuffled.indexOf(text[i]!)];
}
const total = parseInt(text[index]!);
const str = parseInt(text.slice(index + 1, index + total + 1));
return returned_text.slice(text.length - str);
return returned_text.slice(text?.length - str);
};
const extend = (text: string, length = 60) => {
const extra = length - text.length;
const extra = length - text?.length;
if (extra < 0) {
return text;
@ -74,9 +74,9 @@ const extend = (text: string, length = 60) => {
);
const storage_string =
text.length.toString().length.toString() + text.length.toString();
text?.length.toString()?.length.toString() + text?.length.toString();
let returned = "";
let total = storage_string.length + text.length;
let total = storage_string?.length + text?.length;
for (let i = 0; i < extra; i++) {
if (i == index) {
@ -86,7 +86,8 @@ const extend = (text: string, length = 60) => {
break;
}
// Add a random character
returned += shuffled[Math.floor(random(Math.random()) * shuffled.length)];
returned +=
shuffled[Math.floor(random(Math.random()) * shuffled?.length)];
total++;
}
}