mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-10-06 02:47:54 +00:00
added way to save content and vectorize them. also refactored code and added a bypass to browser rendering
This commit is contained in:
parent
84455ad5d5
commit
4daee14a82
31 changed files with 479 additions and 106 deletions
|
|
@ -1 +1 @@
|
|||
Subproject commit b37c962365a36cf342a31a196f4908f4f1343553
|
||||
Subproject commit f1797d84ff322d98041c0909e65ef6db8f7aa2cd
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { Button } from "@repo/ui/shadcn/button";
|
||||
import React from "react";
|
||||
import { signIn } from "../helpers/server/auth";
|
||||
import { signIn } from "../../server/auth";
|
||||
|
||||
function SignIn() {
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import Logo from "@/public/logo.svg";
|
||||
import { signIn } from "@/app/helpers/server/auth";
|
||||
import { signIn } from "@/server/auth";
|
||||
import { Google } from "@repo/ui/components/icons";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ import remarkMath from "remark-math";
|
|||
import rehypeKatex from "rehype-katex";
|
||||
import rehypeHighlight from "rehype-highlight";
|
||||
import { code, p } from "./markdownRenderHelpers";
|
||||
import { codeLanguageSubset } from "@/app/helpers/constants";
|
||||
import { codeLanguageSubset } from "@/lib/constants";
|
||||
|
||||
function ChatWindow({
|
||||
q,
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import ChatWindow from "./chatWindow";
|
||||
import { chatSearchParamsCache } from "../../helpers/lib/searchParams";
|
||||
import { chatSearchParamsCache } from "../../../lib/searchParams";
|
||||
// @ts-expect-error
|
||||
await import("katex/dist/katex.min.css");
|
||||
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import React from "react";
|
|||
import Menu from "../menu";
|
||||
import Header from "../header";
|
||||
import QueryInput from "./queryinput";
|
||||
import { homeSearchParamsCache } from "@/app/helpers/lib/searchParams";
|
||||
import { homeSearchParamsCache } from "@/lib/searchParams";
|
||||
import { getSpaces } from "@/app/actions/fetchers";
|
||||
|
||||
async function Page({
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import Header from "./header";
|
||||
import Menu from "./menu";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "../helpers/server/auth";
|
||||
import { auth } from "../../server/auth";
|
||||
import { Toaster } from "@repo/ui/shadcn/sonner";
|
||||
|
||||
async function Layout({ children }: { children: React.ReactNode }) {
|
||||
|
|
|
|||
|
|
@ -1,14 +1,31 @@
|
|||
"use client";
|
||||
|
||||
import { getAllUserMemoriesAndSpaces } from "@/app/actions/fetchers";
|
||||
import { Space } from "@/app/actions/types";
|
||||
import { Content } from "@/server/db/schema";
|
||||
import { NextIcon, SearchIcon, UrlIcon } from "@repo/ui/icons";
|
||||
import Image from "next/image";
|
||||
import React, { useState } from "react";
|
||||
import React, { useEffect, useState } from "react";
|
||||
|
||||
function page() {
|
||||
const [filter, setFilter] = useState("All")
|
||||
const setFilterfn = (i:string) => setFilter(i)
|
||||
function Page() {
|
||||
const [filter, setFilter] = useState("All");
|
||||
const setFilterfn = (i: string) => setFilter(i);
|
||||
|
||||
const [search, setSearch] = useState("");
|
||||
|
||||
const [memoriesAndSpaces, setMemoriesAndSpaces] = useState<{
|
||||
memories: Content[];
|
||||
spaces: Space[];
|
||||
}>({ memories: [], spaces: [] });
|
||||
|
||||
useEffect(() => {
|
||||
(async () => {
|
||||
const { success, data } = await getAllUserMemoriesAndSpaces();
|
||||
if (!success ?? !data) return;
|
||||
setMemoriesAndSpaces({ memories: data.memories, spaces: data.spaces });
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const [search, setSearch] = useState("")
|
||||
return (
|
||||
<div className="max-w-3xl min-w-3xl py-36 h-full flex mx-auto w-full flex-col gap-12">
|
||||
<h2 className="text-white w-full font-medium text-2xl text-left">
|
||||
|
|
@ -16,41 +33,50 @@ function page() {
|
|||
</h2>
|
||||
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="w-full relative">
|
||||
<input
|
||||
type="text"
|
||||
className=" w-full py-3 rounded-md text-lg pl-8 bg-[#1F2428] outline-none"
|
||||
placeholder="search here..."
|
||||
/>
|
||||
<Image className="absolute top-1/2 -translate-y-1/2 left-2" src={SearchIcon} alt="Search icon" />
|
||||
</div>
|
||||
|
||||
<Filters filter={filter} setFilter={setFilterfn} />
|
||||
<div className="w-full relative">
|
||||
<input
|
||||
type="text"
|
||||
className=" w-full py-3 rounded-md text-lg pl-8 bg-[#1F2428] outline-none"
|
||||
placeholder="search here..."
|
||||
/>
|
||||
<Image
|
||||
className="absolute top-1/2 -translate-y-1/2 left-2"
|
||||
src={SearchIcon}
|
||||
alt="Search icon"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<Filters filter={filter} setFilter={setFilterfn} />
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-[#B3BCC5]">Spaces</div>
|
||||
<TabComponent title="AI Technologies" description="Resources 12" />
|
||||
<TabComponent title="Python Tricks" description="Resources 120" />
|
||||
<TabComponent title="JavaScript Hacks" description="Resources 14" />
|
||||
{memoriesAndSpaces.spaces.map((space) => (
|
||||
<TabComponent title={space.name} description={space.id.toString()} />
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-[#B3BCC5]">Pages</div>
|
||||
<LinkComponent title="How to make a custom AI model?" url="https://google.com" />
|
||||
<LinkComponent title="GPT 5 Release Date" url="https://wth.com" />
|
||||
<LinkComponent title="Why @sama never use uppercase" url="https://tom.com" />
|
||||
{memoriesAndSpaces.memories.map((memory) => (
|
||||
<LinkComponent title={memory.title ?? "No title"} url={memory.url} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TabComponent({title, description}: {title:string, description:string}){
|
||||
function TabComponent({
|
||||
title,
|
||||
description,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex items-center my-6">
|
||||
<div>
|
||||
<div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md">
|
||||
{title.slice(0,2).toUpperCase()}
|
||||
{title.slice(0, 2).toUpperCase()}
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-4">
|
||||
|
|
@ -58,37 +84,50 @@ function TabComponent({title, description}: {title:string, description:string}){
|
|||
<div>{description}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Image src={NextIcon} alt="Search icon" />
|
||||
<Image src={NextIcon} alt="Search icon" />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function LinkComponent({title, url}: {title:string, url:string}){
|
||||
function LinkComponent({ title, url }: { title: string; url: string }) {
|
||||
return (
|
||||
<div className="flex items-center my-6">
|
||||
<div>
|
||||
<div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md">
|
||||
<Image src={UrlIcon} alt="Url icon" />
|
||||
<div>
|
||||
<div className="h-12 w-12 bg-[#1F2428] flex justify-center items-center rounded-md">
|
||||
<Image src={UrlIcon} alt="Url icon" />
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-4">
|
||||
<div className="text-lg text-[#fff]">{title}</div>
|
||||
<div>{url}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grow px-4">
|
||||
<div className="text-lg text-[#fff]">{title}</div>
|
||||
<div>{url}</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
const FilterMethods = ["All", "Spaces", "Pages", "Notes"]
|
||||
function Filters({setFilter, filter}:{setFilter: (i:string)=> void, filter: string}){
|
||||
const FilterMethods = ["All", "Spaces", "Pages", "Notes"];
|
||||
function Filters({
|
||||
setFilter,
|
||||
filter,
|
||||
}: {
|
||||
setFilter: (i: string) => void;
|
||||
filter: string;
|
||||
}) {
|
||||
return (
|
||||
<div className="flex gap-4">
|
||||
{FilterMethods.map((i)=> {
|
||||
return <div onClick={()=> setFilter(i)} className={`transition px-6 py-2 rounded-xl ${i === filter ? "bg-[#21303D] text-[#369DFD]" : "text-[#B3BCC5] bg-[#1F2428] hover:bg-[#1f262d] hover:text-[#76a3cc]"}`}>{i}</div>
|
||||
{FilterMethods.map((i) => {
|
||||
return (
|
||||
<div
|
||||
onClick={() => setFilter(i)}
|
||||
className={`transition px-6 py-2 rounded-xl ${i === filter ? "bg-[#21303D] text-[#369DFD]" : "text-[#B3BCC5] bg-[#1F2428] hover:bg-[#1f262d] hover:text-[#76a3cc]"}`}
|
||||
>
|
||||
{i}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export default page;
|
||||
export default Page;
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ import Cta from "./Cta";
|
|||
import { Toaster } from "@repo/ui/shadcn/toaster";
|
||||
import Features from "./Features";
|
||||
import Footer from "./footer";
|
||||
import { auth } from "../helpers/server/auth";
|
||||
import { auth } from "../../server/auth";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db } from "../helpers/server/db";
|
||||
import { space } from "../helpers/server/db/schema";
|
||||
import { db } from "../../server/db";
|
||||
import { contentToSpace, space, storedContent } from "../../server/db/schema";
|
||||
import { ServerActionReturnType } from "./types";
|
||||
import { auth } from "../helpers/server/auth";
|
||||
import { auth } from "../../server/auth";
|
||||
import { Tweet } from "react-tweet/api";
|
||||
import { getMetaData } from "@/lib/get-metadata";
|
||||
import { and, eq, inArray, sql } from "drizzle-orm";
|
||||
import { LIMITS } from "@/lib/constants";
|
||||
import { z } from "zod";
|
||||
|
||||
export const createSpace = async (
|
||||
input: string | FormData,
|
||||
|
|
@ -41,3 +46,212 @@ export const createSpace = async (
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
const typeDecider = (content: string) => {
|
||||
// if the content is a URL, then it's a page. if its a URL with https://x.com/user/status/123, then it's a tweet. else, it's a note.
|
||||
// do strict checking with regex
|
||||
if (content.match(/https?:\/\/[\w\.]+\/[\w]+\/[\w]+\/[\d]+/)) {
|
||||
return "tweet";
|
||||
} else if (content.match(/https?:\/\/[\w\.]+/)) {
|
||||
return "page";
|
||||
} else {
|
||||
return "note";
|
||||
}
|
||||
};
|
||||
|
||||
export const limit = async (userId: string, type = "page") => {
|
||||
const count = await db
|
||||
.select({
|
||||
count: sql<number>`count(*)`.mapWith(Number),
|
||||
})
|
||||
.from(storedContent)
|
||||
.where(and(eq(storedContent.userId, userId), eq(storedContent.type, type)));
|
||||
|
||||
if (count[0]!.count > LIMITS[type as keyof typeof LIMITS]) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
const getTweetData = async (tweetID: string) => {
|
||||
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, {
|
||||
headers: {
|
||||
"User-Agent":
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.3",
|
||||
Accept: "application/json",
|
||||
"Accept-Language": "en-US,en;q=0.5",
|
||||
"Accept-Encoding": "gzip, deflate, br",
|
||||
Connection: "keep-alive",
|
||||
"Upgrade-Insecure-Requests": "1",
|
||||
"Cache-Control": "max-age=0",
|
||||
TE: "Trailers",
|
||||
},
|
||||
});
|
||||
console.log(resp.status);
|
||||
const data = (await resp.json()) as Tweet;
|
||||
|
||||
return data;
|
||||
};
|
||||
|
||||
export const createMemory = async (input: {
|
||||
content: string;
|
||||
spaces?: string[];
|
||||
}): ServerActionReturnType<number> => {
|
||||
const data = await auth();
|
||||
|
||||
if (!data || !data.user || !data.user.id) {
|
||||
return { error: "Not authenticated", success: false };
|
||||
}
|
||||
|
||||
const type = typeDecider(input.content);
|
||||
|
||||
let pageContent = input.content;
|
||||
let metadata: Awaited<ReturnType<typeof getMetaData>>;
|
||||
|
||||
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.`,
|
||||
};
|
||||
}
|
||||
|
||||
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();
|
||||
metadata = await getMetaData(pageContent);
|
||||
} else if (type === "tweet") {
|
||||
const tweet = await getTweetData(input.content.split("/").pop() as string);
|
||||
pageContent = JSON.stringify(tweet);
|
||||
metadata = {
|
||||
baseUrl: input.content,
|
||||
description: tweet.text,
|
||||
image: tweet.user.profile_image_url_https,
|
||||
title: `Tweet by ${tweet.user.name}`,
|
||||
};
|
||||
} else if (type === "note") {
|
||||
pageContent = input.content;
|
||||
const 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 = [];
|
||||
}
|
||||
|
||||
// Insert into database
|
||||
const insertResponse = await db
|
||||
.insert(storedContent)
|
||||
.values({
|
||||
content: pageContent,
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
url: input.content,
|
||||
baseUrl: metadata.baseUrl,
|
||||
image: metadata.image,
|
||||
savedAt: new Date(),
|
||||
userId: data.user.id,
|
||||
type,
|
||||
})
|
||||
.returning({ id: storedContent.id });
|
||||
|
||||
const contentId = insertResponse[0]?.id;
|
||||
if (!contentId) {
|
||||
return {
|
||||
success: false,
|
||||
data: 0,
|
||||
error: "Something went wrong while saving the document to the database",
|
||||
};
|
||||
}
|
||||
|
||||
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.name, storeToSpaces), eq(space.user, data.user.id)),
|
||||
)
|
||||
.all();
|
||||
|
||||
await Promise.all(
|
||||
spaceData.map(async (space) => {
|
||||
await db
|
||||
.insert(contentToSpace)
|
||||
.values({ contentId: contentId, spaceId: space.id });
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
const vectorSaveResponse = await fetch(
|
||||
`${process.env.BACKEND_BASE_URL}/api/add`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
pageContent,
|
||||
title: metadata.title,
|
||||
description: metadata.description,
|
||||
url: metadata.baseUrl,
|
||||
// TODO: now, in the vector store, we are only saving the first space. We need to save all spaces.
|
||||
space: storeToSpaces[0],
|
||||
user: data.user.id,
|
||||
}),
|
||||
},
|
||||
);
|
||||
|
||||
if (!vectorSaveResponse.ok) {
|
||||
const errorData = await vectorSaveResponse.text();
|
||||
return {
|
||||
success: false,
|
||||
data: 0,
|
||||
error: `Failed to save to vector store. Backend returned error: ${errorData}`,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await vectorSaveResponse.json();
|
||||
|
||||
const expectedResponse = z.object({ status: z.literal("ok") });
|
||||
|
||||
const parsedResponse = expectedResponse.safeParse(response);
|
||||
|
||||
if (!parsedResponse.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: 0,
|
||||
error: `Failed to save to vector store. Backend returned error: ${parsedResponse.error.message}`,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: 1,
|
||||
};
|
||||
} catch (e) {
|
||||
return {
|
||||
success: false,
|
||||
data: 0,
|
||||
error: `Failed to save to vector store. Backend returned error: ${e}`,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,10 +1,15 @@
|
|||
"use server";
|
||||
|
||||
import { eq } from "drizzle-orm";
|
||||
import { db } from "../helpers/server/db";
|
||||
import { users } from "../helpers/server/db/schema";
|
||||
import { eq, inArray, not, sql } from "drizzle-orm";
|
||||
import { db } from "../../server/db";
|
||||
import {
|
||||
Content,
|
||||
contentToSpace,
|
||||
storedContent,
|
||||
users,
|
||||
} from "../../server/db/schema";
|
||||
import { ServerActionReturnType, Space } from "./types";
|
||||
import { auth } from "../helpers/server/auth";
|
||||
import { auth } from "../../server/auth";
|
||||
|
||||
export const getSpaces = async (): ServerActionReturnType<Space[]> => {
|
||||
const data = await auth();
|
||||
|
|
@ -23,3 +28,115 @@ export const getSpaces = async (): ServerActionReturnType<Space[]> => {
|
|||
|
||||
return { success: true, data: spacesWithoutUser };
|
||||
};
|
||||
|
||||
export const getAllMemories = async (
|
||||
freeMemoriesOnly: boolean = false,
|
||||
): ServerActionReturnType<Content[]> => {
|
||||
const data = await auth();
|
||||
|
||||
if (!data || !data.user) {
|
||||
return { error: "Not authenticated", success: false };
|
||||
}
|
||||
|
||||
if (!freeMemoriesOnly) {
|
||||
// Returns all memories, no matter the space.
|
||||
const memories = await db.query.storedContent.findMany({
|
||||
where: eq(users, data.user.id),
|
||||
});
|
||||
|
||||
return { success: true, data: memories };
|
||||
}
|
||||
|
||||
// This only returns memories that are not a part of any space.
|
||||
// This is useful for home page where we want to show a list of spaces and memories.
|
||||
const contentNotInAnySpace = await db
|
||||
.select()
|
||||
.from(storedContent)
|
||||
.where(
|
||||
not(
|
||||
eq(
|
||||
storedContent.id,
|
||||
db
|
||||
.select({ contentId: contentToSpace.contentId })
|
||||
.from(contentToSpace),
|
||||
),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return { success: true, data: contentNotInAnySpace };
|
||||
};
|
||||
|
||||
export const getAllUserMemoriesAndSpaces = async (): ServerActionReturnType<{
|
||||
spaces: Space[];
|
||||
memories: Content[];
|
||||
}> => {
|
||||
const data = await auth();
|
||||
|
||||
if (!data || !data.user) {
|
||||
return { error: "Not authenticated", success: false };
|
||||
}
|
||||
|
||||
const spaces = await db.query.space.findMany({
|
||||
where: eq(users, data.user.id),
|
||||
});
|
||||
|
||||
const spacesWithoutUser = spaces.map((space) => {
|
||||
return { ...space, user: undefined };
|
||||
});
|
||||
|
||||
// const contentCountBySpace = await db
|
||||
// .select({
|
||||
// spaceId: contentToSpace.spaceId,
|
||||
// count: sql<number>`count(*)`.mapWith(Number),
|
||||
// })
|
||||
// .from(contentToSpace)
|
||||
// .where(
|
||||
// inArray(
|
||||
// contentToSpace.spaceId,
|
||||
// spacesWithoutUser.map((space) => space.id),
|
||||
// ),
|
||||
// )
|
||||
// .groupBy(contentToSpace.spaceId)
|
||||
// .execute();
|
||||
|
||||
// console.log(contentCountBySpace);
|
||||
|
||||
// get a count with space mappings like spaceID: count (number of memories in that space)
|
||||
const contentCountBySpace = await db
|
||||
.select({
|
||||
spaceId: contentToSpace.spaceId,
|
||||
count: sql<number>`count(*)`.mapWith(Number),
|
||||
})
|
||||
.from(contentToSpace)
|
||||
.where(
|
||||
inArray(
|
||||
contentToSpace.spaceId,
|
||||
spacesWithoutUser.map((space) => space.id),
|
||||
),
|
||||
)
|
||||
.groupBy(contentToSpace.spaceId)
|
||||
.execute();
|
||||
|
||||
console.log(contentCountBySpace);
|
||||
|
||||
const contentNotInAnySpace = await db
|
||||
.select()
|
||||
.from(storedContent)
|
||||
.where(
|
||||
not(
|
||||
eq(
|
||||
storedContent.id,
|
||||
db
|
||||
.select({ contentId: contentToSpace.contentId })
|
||||
.from(contentToSpace),
|
||||
),
|
||||
),
|
||||
)
|
||||
.execute();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: { spaces: spacesWithoutUser, memories: contentNotInAnySpace },
|
||||
};
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
export type Space = {
|
||||
id: number;
|
||||
name: string;
|
||||
numberOfMemories?: number;
|
||||
};
|
||||
|
||||
export type ServerActionReturnType<T> = Promise<{
|
||||
|
|
|
|||
|
|
@ -1,2 +1,2 @@
|
|||
export { GET, POST } from "../../helpers/server/auth";
|
||||
export { GET, POST } from "../../../server/auth";
|
||||
export const runtime = "edge";
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export async function POST(req: NextRequest) {
|
|||
);
|
||||
|
||||
const resp = await fetch(
|
||||
`https://new-cf-ai-backend.dhravya.workers.dev/api/chat?query=${query}&user=${session.user.email}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
|
||||
`${process.env.BACKEND_BASE_URL}/api/chat?query=${query}&user=${session.user.email}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${process.env.BACKEND_SECURITY_KEY}`,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import { db } from "../helpers/server/db";
|
||||
import { sessions, users } from "../helpers/server/db/schema";
|
||||
import { db } from "../../server/db";
|
||||
import { sessions, users } from "../../server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
|
||||
export async function ensureAuth(req: NextRequest) {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { db } from "@/app/helpers/server/db";
|
||||
import { db } from "@/server/db";
|
||||
import { and, eq, ne, sql } from "drizzle-orm";
|
||||
import { sessions, storedContent, users } from "@/app/helpers/server/db/schema";
|
||||
import { sessions, storedContent, users } from "@/server/db/schema";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { ensureAuth } from "../ensureAuth";
|
||||
|
||||
|
|
@ -20,7 +20,7 @@ export async function GET(req: NextRequest) {
|
|||
.from(storedContent)
|
||||
.where(
|
||||
and(
|
||||
eq(storedContent.user, session.user.id),
|
||||
eq(storedContent.userId, session.user.id),
|
||||
eq(storedContent.type, "twitter-bookmark"),
|
||||
),
|
||||
);
|
||||
|
|
@ -32,7 +32,7 @@ export async function GET(req: NextRequest) {
|
|||
.from(storedContent)
|
||||
.where(
|
||||
and(
|
||||
eq(storedContent.user, session.user.id),
|
||||
eq(storedContent.userId, session.user.id),
|
||||
ne(storedContent.type, "twitter-bookmark"),
|
||||
),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { db } from "@/app/helpers/server/db";
|
||||
import { db } from "@/server/db";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { sessions, users } from "@/app/helpers/server/db/schema";
|
||||
import { sessions, users } from "@/server/db/schema";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { db } from "@/app/helpers/server/db";
|
||||
import { sessions, space, users } from "@/app/helpers/server/db/schema";
|
||||
import { db } from "@/server/db";
|
||||
import { sessions, space, users } from "@/server/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { ensureAuth } from "../ensureAuth";
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { db } from "@/app/helpers/server/db";
|
||||
import { db } from "@/server/db";
|
||||
import { and, eq, sql, inArray } from "drizzle-orm";
|
||||
import {
|
||||
contentToSpace,
|
||||
|
|
@ -6,10 +6,12 @@ import {
|
|||
storedContent,
|
||||
users,
|
||||
space,
|
||||
} from "@/app/helpers/server/db/schema";
|
||||
} from "@/server/db/schema";
|
||||
import { type NextRequest, NextResponse } from "next/server";
|
||||
import { getMetaData } from "@/app/helpers/lib/get-metadata";
|
||||
import { getMetaData } from "@/lib/get-metadata";
|
||||
import { ensureAuth } from "../ensureAuth";
|
||||
import { limit } from "@/app/actions/doers";
|
||||
import { LIMITS } from "@/lib/constants";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
|
|
@ -33,22 +35,13 @@ export async function POST(req: NextRequest) {
|
|||
storeToSpaces = [];
|
||||
}
|
||||
|
||||
const count = await db
|
||||
.select({
|
||||
count: sql<number>`count(*)`.mapWith(Number),
|
||||
})
|
||||
.from(storedContent)
|
||||
.where(
|
||||
and(
|
||||
eq(storedContent.user, session.user.id),
|
||||
eq(storedContent.type, "page"),
|
||||
),
|
||||
);
|
||||
|
||||
if (count[0]!.count > 100) {
|
||||
if (!(await limit(session.user.id))) {
|
||||
return NextResponse.json(
|
||||
{ message: "Error", error: "Limit exceeded" },
|
||||
{ status: 499 },
|
||||
{
|
||||
message: "Error: Ratelimit exceeded",
|
||||
error: `You have exceeded the limit of ${LIMITS["page"]} pages.`,
|
||||
},
|
||||
{ status: 429 },
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -62,7 +55,7 @@ export async function POST(req: NextRequest) {
|
|||
baseUrl: metadata.baseUrl,
|
||||
image: metadata.image,
|
||||
savedAt: new Date(),
|
||||
user: session.user.id,
|
||||
userId: session.user.id,
|
||||
})
|
||||
.returning({ id: storedContent.id });
|
||||
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
import { Button } from "@repo/ui/shadcn/button";
|
||||
import { auth, signIn, signOut } from "../helpers/server/auth";
|
||||
import { db } from "../helpers/server/db";
|
||||
import { auth, signIn, signOut } from "../../server/auth";
|
||||
import { db } from "../../server/db";
|
||||
import { sql } from "drizzle-orm";
|
||||
import { users } from "../helpers/server/db/schema";
|
||||
import { getThemeToggler } from "../helpers/lib/get-theme-button";
|
||||
import { users } from "../../server/db/schema";
|
||||
import { getThemeToggler } from "../../lib/get-theme-button";
|
||||
|
||||
export const runtime = "edge";
|
||||
|
||||
|
|
|
|||
12
apps/web/cf-env.d.ts
vendored
12
apps/web/cf-env.d.ts
vendored
|
|
@ -1,6 +1,16 @@
|
|||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv extends CloudflareEnv {}
|
||||
interface ProcessEnv extends CloudflareEnv {
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_CLIENT_SECRET: string;
|
||||
AUTH_SECRET: string;
|
||||
R2_ENDPOINT: string;
|
||||
R2_ACCESS_ID: string;
|
||||
R2_SECRET_KEY: string;
|
||||
R2_BUCKET_NAME: string;
|
||||
BACKEND_SECURITY_KEY: string;
|
||||
BACKEND_BASE_URL: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
8
apps/web/env.d.ts
vendored
8
apps/web/env.d.ts
vendored
|
|
@ -2,14 +2,6 @@
|
|||
// by running `wrangler types --env-interface CloudflareEnv env.d.ts`
|
||||
|
||||
interface CloudflareEnv {
|
||||
GOOGLE_CLIENT_ID: string;
|
||||
GOOGLE_CLIENT_SECRET: string;
|
||||
AUTH_SECRET: string;
|
||||
R2_ENDPOINT: string;
|
||||
R2_ACCESS_ID: string;
|
||||
R2_SECRET_KEY: string;
|
||||
R2_BUCKET_NAME: string;
|
||||
BACKEND_SECURITY_KEY: string;
|
||||
STORAGE: R2Bucket;
|
||||
DATABASE: D1Database;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
export const LIMITS = {
|
||||
page: 100,
|
||||
tweet: 1000,
|
||||
note: 1000,
|
||||
};
|
||||
|
||||
export const codeLanguageSubset = [
|
||||
"python",
|
||||
"javascript",
|
||||
|
|
@ -103,11 +103,9 @@ export const storedContent = createTable(
|
|||
savedAt: int("savedAt", { mode: "timestamp" }).notNull(),
|
||||
baseUrl: text("baseUrl", { length: 255 }),
|
||||
ogImage: text("ogImage", { length: 255 }),
|
||||
type: text("type", { enum: ["note", "page", "twitter-bookmark"] }).default(
|
||||
"page",
|
||||
),
|
||||
type: text("type").default("page"),
|
||||
image: text("image", { length: 255 }),
|
||||
userId: int("user").references(() => users.id, {
|
||||
userId: text("user").references(() => users.id, {
|
||||
onDelete: "cascade",
|
||||
}),
|
||||
},
|
||||
|
|
@ -119,6 +117,8 @@ export const storedContent = createTable(
|
|||
}),
|
||||
);
|
||||
|
||||
export type Content = typeof storedContent.$inferSelect;
|
||||
|
||||
export const contentToSpace = createTable(
|
||||
"contentToSpace",
|
||||
{
|
||||
|
|
@ -12,7 +12,7 @@
|
|||
"devDependencies": {
|
||||
"@clack/prompts": "^0.7.0",
|
||||
"@cloudflare/next-on-pages": "1",
|
||||
"@cloudflare/workers-types": "^4.20240512.0",
|
||||
"@cloudflare/workers-types": "^4.20240614.0",
|
||||
"@repo/eslint-config": "*",
|
||||
"@repo/tailwind-config": "*",
|
||||
"@repo/typescript-config": "*",
|
||||
|
|
@ -45,7 +45,7 @@
|
|||
"@auth/drizzle-adapter": "^1.1.0",
|
||||
"@aws-sdk/client-s3": "^3.577.0",
|
||||
"@aws-sdk/s3-request-presigner": "^3.577.0",
|
||||
"@cloudflare/puppeteer": "^0.0.8",
|
||||
"@cloudflare/puppeteer": "^0.0.11",
|
||||
"@headlessui/react": "^2.0.4",
|
||||
"@hono/swagger-ui": "^0.2.2",
|
||||
"@hookform/resolvers": "^3.4.2",
|
||||
|
|
@ -78,6 +78,7 @@
|
|||
"react-dropzone": "^14.2.3",
|
||||
"react-hook-form": "^7.51.5",
|
||||
"react-markdown": "^9.0.1",
|
||||
"react-tweet": "^3.2.1",
|
||||
"rehype-highlight": "^7.0.0",
|
||||
"rehype-katex": "^7.0.0",
|
||||
"remark-gfm": "^4.0.0",
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue