aboutsummaryrefslogtreecommitdiff
path: root/apps/web/src
diff options
context:
space:
mode:
authorYash <[email protected]>2024-04-11 04:52:44 +0000
committerYash <[email protected]>2024-04-11 04:52:44 +0000
commit6dcc7d18c9be5e3a5e0a3ff60668424ee0158b4e (patch)
tree179aa936536510cc707368fc7c330c4c7fbdc3f8 /apps/web/src
parentnovel editor (diff)
parentsave user ID with url to ensure that same website can be saved by users (diff)
downloadsupermemory-new-ui.tar.xz
supermemory-new-ui.zip
Merge branch 'main' of https://github.com/Dhravya/supermemory into new-uinew-ui
Diffstat (limited to 'apps/web/src')
-rw-r--r--apps/web/src/app/MessagePoster.tsx14
-rw-r--r--apps/web/src/app/api/ask/route.ts80
-rw-r--r--apps/web/src/app/api/chat/route.ts139
-rw-r--r--apps/web/src/app/api/me/route.ts45
-rw-r--r--apps/web/src/app/api/query/route.ts88
-rw-r--r--apps/web/src/app/api/store/route.ts219
-rw-r--r--apps/web/src/app/content.tsx14
-rw-r--r--apps/web/src/app/globals.css4
-rw-r--r--apps/web/src/app/layout.tsx5
-rw-r--r--apps/web/src/app/page.tsx34
-rw-r--r--apps/web/src/app/privacy/page.tsx6
-rw-r--r--apps/web/src/app/privacy/privacy.ts2
-rw-r--r--apps/web/src/components/Main.tsx8
-rw-r--r--apps/web/src/components/QueryAI.tsx139
-rw-r--r--apps/web/src/components/SearchResults.tsx40
-rw-r--r--apps/web/src/components/Sidebar/CategoryItem.tsx138
-rw-r--r--apps/web/src/components/Sidebar/FilterCombobox.tsx36
-rw-r--r--apps/web/src/components/Sidebar/index.tsx63
-rw-r--r--apps/web/src/components/WordMark.tsx12
-rw-r--r--apps/web/src/components/ui/avatar.tsx26
-rw-r--r--apps/web/src/components/ui/badge.tsx14
-rw-r--r--apps/web/src/components/ui/button.tsx26
-rw-r--r--apps/web/src/components/ui/card.tsx41
-rw-r--r--apps/web/src/components/ui/label.tsx20
-rw-r--r--apps/web/src/contexts/MemoryContext.tsx8
-rw-r--r--apps/web/src/env.js7
-rw-r--r--apps/web/src/lib/searchParams.ts22
-rw-r--r--apps/web/src/server/auth.ts6
-rw-r--r--apps/web/src/server/db/index.ts7
-rw-r--r--apps/web/src/server/db/schema.ts15
-rw-r--r--apps/web/src/server/helpers.ts66
31 files changed, 843 insertions, 501 deletions
diff --git a/apps/web/src/app/MessagePoster.tsx b/apps/web/src/app/MessagePoster.tsx
index 64dc89fd..9b7011a8 100644
--- a/apps/web/src/app/MessagePoster.tsx
+++ b/apps/web/src/app/MessagePoster.tsx
@@ -1,21 +1,21 @@
-'use client';
+"use client";
-import { useEffect } from 'react';
+import { useEffect } from "react";
function MessagePoster({ jwt }: { jwt: string }) {
useEffect(() => {
- if (typeof window === 'undefined') return;
- window.postMessage({ jwt }, '*');
+ if (typeof window === "undefined") return;
+ window.postMessage({ jwt }, "*");
}, [jwt]);
return (
<button
onClick={() => {
- if (typeof window === 'undefined') return;
- window.postMessage({ jwt }, '*');
+ if (typeof window === "undefined") return;
+ window.postMessage({ jwt }, "*");
}}
>
- Send message
+ Extension Auth
</button>
);
}
diff --git a/apps/web/src/app/api/ask/route.ts b/apps/web/src/app/api/ask/route.ts
index cad7a671..89123ac9 100644
--- a/apps/web/src/app/api/ask/route.ts
+++ b/apps/web/src/app/api/ask/route.ts
@@ -7,42 +7,62 @@ import { env } from "@/env";
export const runtime = "edge";
export async function POST(req: NextRequest) {
- const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", "");
+ const token =
+ req.cookies.get("next-auth.session-token")?.value ??
+ req.cookies.get("__Secure-authjs.session-token")?.value ??
+ req.cookies.get("authjs.session-token")?.value ??
+ req.headers.get("Authorization")?.replace("Bearer ", "");
- const sessionData = await db.select().from(sessions).where(eq(sessions.sessionToken, token!))
+ const sessionData = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.sessionToken, token!));
- if (!sessionData || sessionData.length === 0) {
- return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
- }
+ if (!sessionData || sessionData.length === 0) {
+ return new Response(
+ JSON.stringify({ message: "Invalid Key, session not found." }),
+ { status: 404 },
+ );
+ }
- const user = await db.select().from(users).where(eq(users.id, sessionData[0].userId)).limit(1)
+ const user = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, sessionData[0].userId))
+ .limit(1);
- if (!user || user.length === 0) {
- return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 });
- }
+ if (!user || user.length === 0) {
+ return NextResponse.json(
+ { message: "Invalid Key, session not found." },
+ { status: 404 },
+ );
+ }
- const body = await req.json() as {
- query: string;
- }
+ const body = (await req.json()) as {
+ query: string;
+ };
- const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/ask`, {
- headers: {
- "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
- },
- method: "POST",
- body: JSON.stringify({
- query: body.query,
- }),
- })
+ const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/ask`, {
+ headers: {
+ "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
+ },
+ method: "POST",
+ body: JSON.stringify({
+ query: body.query,
+ }),
+ });
- if (resp.status !== 200 || !resp.ok) {
- const errorData = await resp.json();
- return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status });
- }
+ if (resp.status !== 200 || !resp.ok) {
+ const errorData = await resp.json();
+ return new Response(
+ JSON.stringify({ message: "Error in CF function", error: errorData }),
+ { status: resp.status },
+ );
+ }
- // Stream the response back to the client
- const { readable, writable } = new TransformStream();
- resp && resp.body!.pipeTo(writable);
+ // Stream the response back to the client
+ const { readable, writable } = new TransformStream();
+ resp && resp.body!.pipeTo(writable);
- return new Response(readable, { status: 200 });
-} \ No newline at end of file
+ return new Response(readable, { status: 200 });
+}
diff --git a/apps/web/src/app/api/chat/route.ts b/apps/web/src/app/api/chat/route.ts
index aec5b0ea..bc7a4ee4 100644
--- a/apps/web/src/app/api/chat/route.ts
+++ b/apps/web/src/app/api/chat/route.ts
@@ -8,60 +8,85 @@ import { ChatHistory } from "../../../../types/memory";
export const runtime = "edge";
export async function POST(req: NextRequest) {
- const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", "");
-
- const sessionData = await db.select().from(sessions).where(eq(sessions.sessionToken, token!))
-
- if (!sessionData || sessionData.length === 0) {
- return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
- }
-
- const user = await db.select().from(users).where(eq(users.id, sessionData[0].userId)).limit(1)
-
- if (!user || user.length === 0) {
- return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 });
- }
-
- const session = { session: sessionData[0], user: user[0] }
-
- const query = new URL(req.url).searchParams.get("q");
- const spaces = new URL(req.url).searchParams.get("spaces");
-
- const sourcesOnly = new URL(req.url).searchParams.get("sourcesOnly") ?? "false";
-
- const chatHistory = await req.json() as {
- chatHistory: ChatHistory[]
- };
-
- console.log("CHathistory", chatHistory)
-
- if (!query) {
- return new Response(JSON.stringify({ message: "Invalid query" }), { status: 400 });
- }
-
-
- const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`, {
- headers: {
- "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
- },
- method: "POST",
- body: JSON.stringify({
- chatHistory: chatHistory.chatHistory ?? []
- })
- })
-
- console.log(resp.status)
- console.log(resp.statusText)
-
- if (resp.status !== 200 || !resp.ok) {
- const errorData = await resp.json();
- console.log(errorData)
- return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status });
- }
-
- // Stream the response back to the client
- const { readable, writable } = new TransformStream();
- resp && resp.body!.pipeTo(writable);
-
- return new Response(readable, { status: 200 });
-} \ No newline at end of file
+ const token =
+ req.cookies.get("next-auth.session-token")?.value ??
+ req.cookies.get("__Secure-authjs.session-token")?.value ??
+ req.cookies.get("authjs.session-token")?.value ??
+ req.headers.get("Authorization")?.replace("Bearer ", "");
+
+ const sessionData = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.sessionToken, token!));
+
+ if (!sessionData || sessionData.length === 0) {
+ return new Response(
+ JSON.stringify({ message: "Invalid Key, session not found." }),
+ { status: 404 },
+ );
+ }
+
+ const user = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, sessionData[0].userId))
+ .limit(1);
+
+ if (!user || user.length === 0) {
+ return NextResponse.json(
+ { message: "Invalid Key, session not found." },
+ { status: 404 },
+ );
+ }
+
+ const session = { session: sessionData[0], user: user[0] };
+
+ const query = new URL(req.url).searchParams.get("q");
+ const spaces = new URL(req.url).searchParams.get("spaces");
+
+ const sourcesOnly =
+ new URL(req.url).searchParams.get("sourcesOnly") ?? "false";
+
+ const chatHistory = (await req.json()) as {
+ chatHistory: ChatHistory[];
+ };
+
+ console.log("CHathistory", chatHistory);
+
+ if (!query) {
+ return new Response(JSON.stringify({ message: "Invalid query" }), {
+ status: 400,
+ });
+ }
+
+ const resp = await fetch(
+ `https://cf-ai-backend.dhravya.workers.dev/chat?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
+ {
+ headers: {
+ "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
+ },
+ method: "POST",
+ body: JSON.stringify({
+ chatHistory: chatHistory.chatHistory ?? [],
+ }),
+ },
+ );
+
+ console.log(resp.status);
+ console.log(resp.statusText);
+
+ if (resp.status !== 200 || !resp.ok) {
+ const errorData = await resp.json();
+ console.log(errorData);
+ return new Response(
+ JSON.stringify({ message: "Error in CF function", error: errorData }),
+ { status: resp.status },
+ );
+ }
+
+ // Stream the response back to the client
+ const { readable, writable } = new TransformStream();
+ resp && resp.body!.pipeTo(writable);
+
+ return new Response(readable, { status: 200 });
+}
diff --git a/apps/web/src/app/api/me/route.ts b/apps/web/src/app/api/me/route.ts
index a2e713e1..6d269872 100644
--- a/apps/web/src/app/api/me/route.ts
+++ b/apps/web/src/app/api/me/route.ts
@@ -7,19 +7,42 @@ import { env } from "@/env";
export const runtime = "edge";
export async function GET(req: NextRequest) {
- const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", "");
+ const token =
+ req.cookies.get("next-auth.session-token")?.value ??
+ req.cookies.get("__Secure-authjs.session-token")?.value ??
+ req.cookies.get("authjs.session-token")?.value ??
+ req.headers.get("Authorization")?.replace("Bearer ", "");
- const session = await db.select().from(sessions).where(eq(sessions.sessionToken, token!))
+ const session = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.sessionToken, token!));
- if (!session || session.length === 0) {
- return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
- }
+ if (!session || session.length === 0) {
+ return new Response(
+ JSON.stringify({ message: "Invalid Key, session not found." }),
+ { status: 404 },
+ );
+ }
- const user = await db.select().from(users).where(eq(users.id, session[0].userId)).limit(1)
+ const user = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, session[0].userId))
+ .limit(1);
- if (!user || user.length === 0) {
- return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 });
- }
+ if (!user || user.length === 0) {
+ return NextResponse.json(
+ { message: "Invalid Key, session not found." },
+ { status: 404 },
+ );
+ }
- return new Response(JSON.stringify({ message: "OK", data: { session: session[0], user: user[0] } }), { status: 200 });
-} \ No newline at end of file
+ return new Response(
+ JSON.stringify({
+ message: "OK",
+ data: { session: session[0], user: user[0] },
+ }),
+ { status: 200 },
+ );
+}
diff --git a/apps/web/src/app/api/query/route.ts b/apps/web/src/app/api/query/route.ts
index 28f441bc..02bb79da 100644
--- a/apps/web/src/app/api/query/route.ts
+++ b/apps/web/src/app/api/query/route.ts
@@ -7,46 +7,72 @@ import { env } from "@/env";
export const runtime = "edge";
export async function GET(req: NextRequest) {
- const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", "");
+ const token =
+ req.cookies.get("next-auth.session-token")?.value ??
+ req.cookies.get("__Secure-authjs.session-token")?.value ??
+ req.cookies.get("authjs.session-token")?.value ??
+ req.headers.get("Authorization")?.replace("Bearer ", "");
- const sessionData = await db.select().from(sessions).where(eq(sessions.sessionToken, token!))
+ const sessionData = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.sessionToken, token!));
- if (!sessionData || sessionData.length === 0) {
- return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
- }
+ if (!sessionData || sessionData.length === 0) {
+ return new Response(
+ JSON.stringify({ message: "Invalid Key, session not found." }),
+ { status: 404 },
+ );
+ }
- const user = await db.select().from(users).where(eq(users.id, sessionData[0].userId)).limit(1)
+ const user = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, sessionData[0].userId))
+ .limit(1);
- if (!user || user.length === 0) {
- return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 });
- }
+ if (!user || user.length === 0) {
+ return NextResponse.json(
+ { message: "Invalid Key, session not found." },
+ { status: 404 },
+ );
+ }
- const session = { session: sessionData[0], user: user[0] }
+ const session = { session: sessionData[0], user: user[0] };
- const query = new URL(req.url).searchParams.get("q");
- const sourcesOnly = new URL(req.url).searchParams.get("sourcesOnly") ?? "false";
+ const query = new URL(req.url).searchParams.get("q");
+ const sourcesOnly =
+ new URL(req.url).searchParams.get("sourcesOnly") ?? "false";
- if (!query) {
- return new Response(JSON.stringify({ message: "Invalid query" }), { status: 400 });
- }
+ if (!query) {
+ return new Response(JSON.stringify({ message: "Invalid query" }), {
+ status: 400,
+ });
+ }
- const resp = await fetch(`https://cf-ai-backend.dhravya.workers.dev/query?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}`, {
- headers: {
- "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
- }
- })
+ const resp = await fetch(
+ `https://cf-ai-backend.dhravya.workers.dev/query?q=${query}&user=${session.user.email ?? session.user.name}&sourcesOnly=${sourcesOnly}`,
+ {
+ headers: {
+ "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
+ },
+ },
+ );
- console.log(resp.status)
+ console.log(resp.status);
- if (resp.status !== 200 || !resp.ok) {
- const errorData = await resp.json();
- console.log(errorData)
- return new Response(JSON.stringify({ message: "Error in CF function", error: errorData }), { status: resp.status });
- }
+ if (resp.status !== 200 || !resp.ok) {
+ const errorData = await resp.json();
+ console.log(errorData);
+ return new Response(
+ JSON.stringify({ message: "Error in CF function", error: errorData }),
+ { status: resp.status },
+ );
+ }
- // Stream the response back to the client
- const { readable, writable } = new TransformStream();
- resp && resp.body!.pipeTo(writable);
+ // Stream the response back to the client
+ const { readable, writable } = new TransformStream();
+ resp && resp.body!.pipeTo(writable);
- return new Response(readable, { status: 200 });
-} \ No newline at end of file
+ return new Response(readable, { status: 200 });
+}
diff --git a/apps/web/src/app/api/store/route.ts b/apps/web/src/app/api/store/route.ts
index 06db08b9..ebe23077 100644
--- a/apps/web/src/app/api/store/route.ts
+++ b/apps/web/src/app/api/store/route.ts
@@ -1,6 +1,12 @@
import { db } from "@/server/db";
import { and, eq } from "drizzle-orm";
-import { contentToSpace, sessions, storedContent, users, space } from "@/server/db/schema";
+import {
+ contentToSpace,
+ sessions,
+ storedContent,
+ users,
+ space,
+} from "@/server/db/schema";
import { type NextRequest, NextResponse } from "next/server";
import { env } from "@/env";
import { getMetaData } from "@/server/helpers";
@@ -8,94 +14,123 @@ import { getMetaData } from "@/server/helpers";
export const runtime = "edge";
export async function POST(req: NextRequest) {
- const token = req.cookies.get("next-auth.session-token")?.value ?? req.cookies.get("__Secure-authjs.session-token")?.value ?? req.cookies.get("authjs.session-token")?.value ?? req.headers.get("Authorization")?.replace("Bearer ", "");
-
- if (!token) {
- return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
- }
-
- const sessionData = await db.select().from(sessions).where(eq(sessions.sessionToken, token!))
-
- if (!sessionData || sessionData.length === 0) {
- return new Response(JSON.stringify({ message: "Invalid Key, session not found." }), { status: 404 });
- }
-
- const user = await db.select().from(users).where(eq(users.id, sessionData[0].userId)).limit(1)
-
- if (!user || user.length === 0) {
- return NextResponse.json({ message: "Invalid Key, session not found." }, { status: 404 });
- }
-
- const session = { session: sessionData[0], user: user[0] }
-
- const data = await req.json() as {
- pageContent: string,
- url: string,
- space?: string
- };
-
- const metadata = await getMetaData(data.url);
-
-
- let id: number | undefined = undefined;
-
- let storeToSpace = data.space
-
- if (!storeToSpace) {
- storeToSpace = 'all'
- }
-
- const storedContentId = await db.insert(storedContent).values({
- content: data.pageContent,
- title: metadata.title,
- description: metadata.description,
- url: data.url,
- baseUrl: metadata.baseUrl,
- image: metadata.image,
- savedAt: new Date(),
- user: session.user.id
- })
-
- id = storedContentId.meta.last_row_id;
-
- if (!id) {
- return NextResponse.json({ message: "Error", error: "Error in CF function" }, { status: 500 });
- }
-
- let spaceID = 0;
-
- const spaceData = await db.select().from(space).where(and(eq(space.name, storeToSpace), eq(space.user, session.user.id))).limit(1)
- spaceID = spaceData[0]?.id
-
- if (!spaceData || spaceData.length === 0) {
- const spaceId = await db.insert(space).values({
- name: storeToSpace,
- user: session.user.id
- })
- spaceID = spaceId.meta.last_row_id;
- }
-
- await db.insert(contentToSpace).values({
- contentId: id as number,
- spaceId: spaceID
- })
-
- const res = await Promise.race([
- fetch("https://cf-ai-backend.dhravya.workers.dev/add", {
- method: "POST",
- headers: {
- "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
- },
- body: JSON.stringify({ ...data, user: session.user.email }),
- }),
- new Promise((_, reject) =>
- setTimeout(() => reject(new Error('Request timed out')), 40000)
- )
- ]) as Response
-
- if (res.status !== 200) {
- return NextResponse.json({ message: "Error", error: "Error in CF function" }, { status: 500 });
- }
-
- return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 });
-} \ No newline at end of file
+ const token =
+ req.cookies.get("next-auth.session-token")?.value ??
+ req.cookies.get("__Secure-authjs.session-token")?.value ??
+ req.cookies.get("authjs.session-token")?.value ??
+ req.headers.get("Authorization")?.replace("Bearer ", "");
+
+ if (!token) {
+ return new Response(
+ JSON.stringify({ message: "Invalid Key, session not found." }),
+ { status: 404 },
+ );
+ }
+
+ const sessionData = await db
+ .select()
+ .from(sessions)
+ .where(eq(sessions.sessionToken, token!));
+
+ if (!sessionData || sessionData.length === 0) {
+ return new Response(
+ JSON.stringify({ message: "Invalid Key, session not found." }),
+ { status: 404 },
+ );
+ }
+
+ const user = await db
+ .select()
+ .from(users)
+ .where(eq(users.id, sessionData[0].userId))
+ .limit(1);
+
+ if (!user || user.length === 0) {
+ return NextResponse.json(
+ { message: "Invalid Key, session not found." },
+ { status: 404 },
+ );
+ }
+
+ const session = { session: sessionData[0], user: user[0] };
+
+ const data = (await req.json()) as {
+ pageContent: string;
+ url: string;
+ space?: string;
+ };
+
+ const metadata = await getMetaData(data.url);
+
+ let id: number | undefined = undefined;
+
+ let storeToSpace = data.space;
+
+ if (!storeToSpace) {
+ storeToSpace = "all";
+ }
+
+ const storedContentId = await db.insert(storedContent).values({
+ content: data.pageContent,
+ title: metadata.title,
+ description: metadata.description,
+ url: data.url,
+ baseUrl: metadata.baseUrl,
+ image: metadata.image,
+ savedAt: new Date(),
+ user: session.user.id,
+ });
+
+ id = storedContentId.meta.last_row_id;
+
+ if (!id) {
+ return NextResponse.json(
+ { message: "Error", error: "Error in CF function" },
+ { status: 500 },
+ );
+ }
+
+ let spaceID = 0;
+
+ const spaceData = await db
+ .select()
+ .from(space)
+ .where(and(eq(space.name, storeToSpace), eq(space.user, session.user.id)))
+ .limit(1);
+ spaceID = spaceData[0]?.id;
+
+ if (!spaceData || spaceData.length === 0) {
+ const spaceId = await db.insert(space).values({
+ name: storeToSpace,
+ user: session.user.id,
+ });
+ spaceID = spaceId.meta.last_row_id;
+ }
+
+ await db.insert(contentToSpace).values({
+ contentId: id as number,
+ spaceId: spaceID,
+ });
+
+ const res = (await Promise.race([
+ fetch("https://cf-ai-backend.dhravya.workers.dev/add", {
+ method: "POST",
+ headers: {
+ "X-Custom-Auth-Key": env.BACKEND_SECURITY_KEY,
+ },
+ body: JSON.stringify({ ...data, user: session.user.email }),
+ }),
+ new Promise((_, reject) =>
+ setTimeout(() => reject(new Error("Request timed out")), 40000),
+ ),
+ ])) as Response;
+
+ if (res.status !== 200) {
+ return NextResponse.json(
+ { message: "Error", error: "Error in CF function" },
+ { status: 500 },
+ );
+ }
+
+ return NextResponse.json({ message: "OK", data: "Success" }, { status: 200 });
+}
diff --git a/apps/web/src/app/content.tsx b/apps/web/src/app/content.tsx
index 39f2948d..50e0617c 100644
--- a/apps/web/src/app/content.tsx
+++ b/apps/web/src/app/content.tsx
@@ -1,16 +1,16 @@
-'use client';
-import Main from '@/components/Main';
-import Sidebar from '@/components/Sidebar/index';
-import { SessionProvider } from 'next-auth/react';
-import { useState } from 'react';
+"use client";
+import Main from "@/components/Main";
+import Sidebar from "@/components/Sidebar/index";
+import { SessionProvider } from "next-auth/react";
+import { useState } from "react";
-export default function Content() {
+export default function Content({ jwt }: { jwt: string }) {
const [selectedItem, setSelectedItem] = useState<string | null>(null);
return (
<SessionProvider>
<div className="flex w-screen">
- <Sidebar selectChange={setSelectedItem} />
+ <Sidebar jwt={jwt} selectChange={setSelectedItem} />
<Main sidebarOpen={selectedItem !== null} />
</div>
</SessionProvider>
diff --git a/apps/web/src/app/globals.css b/apps/web/src/app/globals.css
index cedb03dc..23caee5b 100644
--- a/apps/web/src/app/globals.css
+++ b/apps/web/src/app/globals.css
@@ -20,7 +20,7 @@
}
body {
- @apply bg-rgray-2 text-rgray-11 max-h-screen overflow-y-hidden;
+ @apply text-rgray-11 max-h-screen overflow-y-hidden bg-white;
/* color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
@@ -66,7 +66,7 @@ body {
}
.chat-answer h1 {
- @apply text-rgray-11 text-xl font-medium my-5;
+ @apply text-rgray-11 my-5 text-xl font-medium;
}
.chat-answer img {
diff --git a/apps/web/src/app/layout.tsx b/apps/web/src/app/layout.tsx
index 1b204dec..42485461 100644
--- a/apps/web/src/app/layout.tsx
+++ b/apps/web/src/app/layout.tsx
@@ -1,8 +1,9 @@
import type { Metadata } from "next";
-import { Roboto } from "next/font/google";
+import { Roboto, Inter } from "next/font/google";
import "./globals.css";
const roboto = Roboto({ weight: ["300", "400", "500"], subsets: ["latin"] });
+const inter = Inter({ weight: ["300", "400", "500"], subsets: ["latin"] });
export const metadata: Metadata = {
title: "Create Next App",
@@ -16,7 +17,7 @@ export default function RootLayout({
}>) {
return (
<html lang="en" className="dark">
- <body className={roboto.className}>
+ <body className={inter.className}>
<div vaul-drawer-wrapper="" className="min-w-screen overflow-x-hidden">
{children}
</div>
diff --git a/apps/web/src/app/page.tsx b/apps/web/src/app/page.tsx
index 95e18e83..6f9ca753 100644
--- a/apps/web/src/app/page.tsx
+++ b/apps/web/src/app/page.tsx
@@ -59,26 +59,26 @@ export default async function Home() {
const collectedSpaces =
contents.length > 0 ? await transformContent(contents) : [];
- collectedSpaces.push({
- id: 2,
- title: "Test",
- content: [
- {
- id: 1,
- content: "Test",
- title: "Vscode",
- description: "Test",
- url: "https://vscode-remake.vercel.app/",
- savedAt: new Date(),
- baseUrl: "https://vscode-remake.vercel.app/",
- image: "https://vscode-remake.vercel.app/favicon.svg",
- },
- ],
- });
+ // collectedSpaces.push({
+ // id: 2,
+ // title: "Test",
+ // content: [
+ // {
+ // id: 1,
+ // content: "Test",
+ // title: "Vscode",
+ // description: "Test",
+ // url: "https://vscode-remake.vercel.app/",
+ // savedAt: new Date(),
+ // baseUrl: "https://vscode-remake.vercel.app/",
+ // image: "https://vscode-remake.vercel.app/favicon.svg",
+ // },
+ // ],
+ // });
return (
<MemoryProvider spaces={collectedSpaces}>
- <Content />
+ <Content jwt={token} />
{/* <MessagePoster jwt={token} /> */}
</MemoryProvider>
);
diff --git a/apps/web/src/app/privacy/page.tsx b/apps/web/src/app/privacy/page.tsx
index 5d0ae2b8..8d126dff 100644
--- a/apps/web/src/app/privacy/page.tsx
+++ b/apps/web/src/app/privacy/page.tsx
@@ -1,6 +1,6 @@
-import React from 'react';
-import Markdown from 'react-markdown';
-import { policy } from './privacy';
+import React from "react";
+import Markdown from "react-markdown";
+import { policy } from "./privacy";
function Page() {
return (
diff --git a/apps/web/src/app/privacy/privacy.ts b/apps/web/src/app/privacy/privacy.ts
index 3e3df4fb..2034f191 100644
--- a/apps/web/src/app/privacy/privacy.ts
+++ b/apps/web/src/app/privacy/privacy.ts
@@ -46,4 +46,4 @@ If you have any questions about this Privacy Policy, the practices of this site,
-This document was last updated on March 2, 2024.` \ No newline at end of file
+This document was last updated on March 2, 2024.`;
diff --git a/apps/web/src/components/Main.tsx b/apps/web/src/components/Main.tsx
index c621c68f..b34755f9 100644
--- a/apps/web/src/components/Main.tsx
+++ b/apps/web/src/components/Main.tsx
@@ -11,6 +11,7 @@ import { ChatHistory } from "../../types/memory";
import { ChatAnswer, ChatMessage, ChatQuestion } from "./ChatMessage";
import { useRouter, useSearchParams } from "next/navigation";
import { useMemory } from "@/contexts/MemoryContext";
+import WordMark from "./WordMark";
function supportsDVH() {
try {
@@ -293,8 +294,8 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
hide ? "" : "main-hidden",
)}
>
- <h1 className="text-rgray-11 mt-auto w-full text-center text-3xl md:mt-0">
- Ask your Second brain
+ <h1 className="text-rgray-11 mt-auto w-full text-center text-3xl font-bold tracking-tight md:mt-0">
+ Ask your second brain
</h1>
<Textarea2
@@ -308,14 +309,13 @@ export default function Main({ sidebarOpen }: { sidebarOpen: boolean }) {
duration: 0.2,
}}
textAreaProps={{
- placeholder: "Ask your SuperMemory...",
+ placeholder: "Ask your second brain...",
className:
"h-auto overflow-auto md:h-full md:resize-none text-lg py-0 px-2 pt-2 md:py-0 md:p-5 resize-y text-rgray-11 w-full min-h-[1em]",
value,
autoFocus: true,
onChange: (e) => setValue(e.target.value),
onKeyDown: (e) => {
- console.log(e.key, e.ctrlKey, e.metaKey);
if (e.key === "Enter" && (e.ctrlKey || e.metaKey)) {
onSend();
}
diff --git a/apps/web/src/components/QueryAI.tsx b/apps/web/src/components/QueryAI.tsx
new file mode 100644
index 00000000..894b5d2d
--- /dev/null
+++ b/apps/web/src/components/QueryAI.tsx
@@ -0,0 +1,139 @@
+"use client";
+
+import { Label } from "./ui/label";
+import React, { useEffect, useState } from "react";
+import { Input } from "./ui/input";
+import { Button } from "./ui/button";
+import SearchResults from "./SearchResults";
+
+function QueryAI() {
+ const [searchResults, setSearchResults] = useState<string[]>([]);
+ const [isAiLoading, setIsAiLoading] = useState(false);
+
+ const [aiResponse, setAIResponse] = useState("");
+ const [input, setInput] = useState("");
+ const [toBeParsed, setToBeParsed] = useState("");
+
+ const handleStreamData = (newChunk: string) => {
+ // Append the new chunk to the existing data to be parsed
+ setToBeParsed((prev) => prev + newChunk);
+ };
+
+ useEffect(() => {
+ // Define a function to try parsing the accumulated data
+ const tryParseAccumulatedData = () => {
+ // Attempt to parse the "toBeParsed" state as JSON
+ try {
+ // Split the accumulated data by the known delimiter "\n\n"
+ const parts = toBeParsed.split("\n\n");
+ let remainingData = "";
+
+ // Process each part to extract JSON objects
+ parts.forEach((part, index) => {
+ try {
+ const parsedPart = JSON.parse(part.replace("data: ", "")); // Try to parse the part as JSON
+
+ // If the part is the last one and couldn't be parsed, keep it to accumulate more data
+ if (index === parts.length - 1 && !parsedPart) {
+ remainingData = part;
+ } else if (parsedPart && parsedPart.response) {
+ // If the part is parsable and has the "response" field, update the AI response state
+ setAIResponse((prev) => prev + parsedPart.response);
+ }
+ } catch (error) {
+ // If parsing fails and it's not the last part, it's a malformed JSON
+ if (index !== parts.length - 1) {
+ console.error("Malformed JSON part: ", part);
+ } else {
+ // If it's the last part, it may be incomplete, so keep it
+ remainingData = part;
+ }
+ }
+ });
+
+ // Update the toBeParsed state to only contain the unparsed remainder
+ if (remainingData !== toBeParsed) {
+ setToBeParsed(remainingData);
+ }
+ } catch (error) {
+ console.error("Error parsing accumulated data: ", error);
+ }
+ };
+
+ // Call the parsing function if there's data to be parsed
+ if (toBeParsed) {
+ tryParseAccumulatedData();
+ }
+ }, [toBeParsed]);
+
+ const getSearchResults = async (e: React.FormEvent<HTMLFormElement>) => {
+ e.preventDefault();
+ setIsAiLoading(true);
+
+ const sourcesResponse = await fetch(
+ `/api/query?sourcesOnly=true&q=${input}`,
+ );
+
+ const sourcesInJson = (await sourcesResponse.json()) as {
+ ids: string[];
+ };
+
+ setSearchResults(sourcesInJson.ids);
+
+ const response = await fetch(`/api/query?q=${input}`);
+
+ if (response.status !== 200) {
+ setIsAiLoading(false);
+ return;
+ }
+
+ if (response.body) {
+ let reader = response.body.getReader();
+ let decoder = new TextDecoder("utf-8");
+ let result = "";
+
+ // @ts-ignore
+ reader.read().then(function processText({ done, value }) {
+ if (done) {
+ // setSearchResults(JSON.parse(result.replace('data: ', '')));
+ // setIsAiLoading(false);
+ return;
+ }
+
+ handleStreamData(decoder.decode(value));
+
+ return reader.read().then(processText);
+ });
+ }
+ };
+
+ return (
+ <div className="mx-auto w-full max-w-2xl">
+ <form onSubmit={async (e) => await getSearchResults(e)} className="mt-8">
+ <Label htmlFor="searchInput">Ask your SuperMemory</Label>
+ <div className="flex flex-col space-y-2 md:w-full md:flex-row md:items-center md:space-x-2 md:space-y-0">
+ <Input
+ value={input}
+ onChange={(e) => setInput(e.target.value)}
+ placeholder="Search using AI... ✨"
+ id="searchInput"
+ />
+ <Button
+ disabled={isAiLoading}
+ className="max-w-min md:w-full"
+ type="submit"
+ variant="default"
+ >
+ Ask AI
+ </Button>
+ </div>
+ </form>
+
+ {searchResults && (
+ <SearchResults aiResponse={aiResponse} sources={searchResults} />
+ )}
+ </div>
+ );
+}
+
+export default QueryAI;
diff --git a/apps/web/src/components/SearchResults.tsx b/apps/web/src/components/SearchResults.tsx
new file mode 100644
index 00000000..d348814e
--- /dev/null
+++ b/apps/web/src/components/SearchResults.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import React from "react";
+import { Card, CardContent } from "./ui/card";
+import Markdown from "react-markdown";
+import remarkGfm from "remark-gfm";
+
+function SearchResults({
+ aiResponse,
+ sources,
+}: {
+ aiResponse: string;
+ sources: string[];
+}) {
+ return (
+ <div
+ style={{
+ backgroundImage: `linear-gradient(to right, #E5D9F2, #CDC1FF)`,
+ }}
+ className="mx-auto mt-4 w-full max-w-2xl space-y-6 rounded-xl border px-4 py-6"
+ >
+ <div className="text-start">
+ <div className="text-xl text-black">
+ <Markdown remarkPlugins={[remarkGfm]}>
+ {aiResponse.replace("</s>", "")}
+ </Markdown>
+ </div>
+ </div>
+ <div className="grid gap-6">
+ {sources.map((value, index) => (
+ <Card key={index}>
+ <CardContent className="space-y-2">{value}</CardContent>
+ </Card>
+ ))}
+ </div>
+ </div>
+ );
+}
+
+export default SearchResults;
diff --git a/apps/web/src/components/Sidebar/CategoryItem.tsx b/apps/web/src/components/Sidebar/CategoryItem.tsx
index 0cf8a70c..7fb571b5 100644
--- a/apps/web/src/components/Sidebar/CategoryItem.tsx
+++ b/apps/web/src/components/Sidebar/CategoryItem.tsx
@@ -1,13 +1,13 @@
-'use client';
-import { cleanUrl } from '@/lib/utils';
-import { StoredContent } from '@/server/db/schema';
+"use client";
+import { cleanUrl } from "@/lib/utils";
+import { StoredContent } from "@/server/db/schema";
import {
DropdownMenu,
DropdownMenuTrigger,
DropdownMenuContent,
DropdownMenuItem,
-} from '../ui/dropdown-menu';
-import { Label } from '../ui/label';
+} from "../ui/dropdown-menu";
+import { Label } from "../ui/label";
import {
ArrowUpRight,
MoreHorizontal,
@@ -19,8 +19,8 @@ import {
ChevronRight,
Plus,
Minus,
-} from 'lucide-react';
-import { useState } from 'react';
+} from "lucide-react";
+import { useState } from "react";
import {
Drawer,
DrawerContent,
@@ -29,106 +29,106 @@ import {
DrawerDescription,
DrawerFooter,
DrawerClose,
-} from '../ui/drawer';
-import { Input } from '../ui/input';
-import { Textarea } from '../ui/textarea';
-import { Popover, PopoverContent, PopoverTrigger } from '../ui/popover';
+} from "../ui/drawer";
+import { Input } from "../ui/input";
+import { Textarea } from "../ui/textarea";
+import { Popover, PopoverContent, PopoverTrigger } from "../ui/popover";
import {
AnimatePresence,
motion,
Reorder,
useMotionValue,
-} from 'framer-motion';
+} from "framer-motion";
const pages: StoredContent[] = [
{
id: 1,
- content: '',
- title: 'Visual Studio Code',
- url: 'https://code.visualstudio.com',
- description: '',
- image: 'https://code.visualstudio.com/favicon.ico',
- baseUrl: 'https://code.visualstudio.com',
+ content: "",
+ title: "Visual Studio Code",
+ url: "https://code.visualstudio.com",
+ description: "",
+ image: "https://code.visualstudio.com/favicon.ico",
+ baseUrl: "https://code.visualstudio.com",
savedAt: new Date(),
},
{
id: 2,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 3,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 4,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 5,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 6,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 7,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 8,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
{
id: 9,
- content: '',
+ content: "",
title: "yxshv/vscode: An unofficial remake of vscode's landing page",
- url: 'https://github.com/yxshv/vscode',
- description: '',
- image: 'https://github.com/favicon.ico',
- baseUrl: 'https://github.com',
+ url: "https://github.com/yxshv/vscode",
+ description: "",
+ image: "https://github.com/favicon.ico",
+ baseUrl: "https://github.com",
savedAt: new Date(),
},
];
@@ -153,13 +153,13 @@ export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => {
/>
<ChevronDown
data-down-icon
- className={`absolute left-1/2 top-1/2 z-[2] h-4 w-4 min-w-4 -translate-x-1/2 -translate-y-1/2 scale-75 opacity-0 transition-[transform,opacity] duration-150 ${isExpanded ? 'rotate-180' : 'rotate-0'}`}
+ className={`absolute left-1/2 top-1/2 z-[2] h-4 w-4 min-w-4 -translate-x-1/2 -translate-y-1/2 scale-75 opacity-0 transition-[transform,opacity] duration-150 ${isExpanded ? "rotate-180" : "rotate-0"}`}
strokeWidth={1.5}
/>
</div>
<span className="w-full truncate text-nowrap text-left">
- {item.title ?? 'Untitled website'}
+ {item.title ?? "Untitled website"}
</span>
</button>
<Drawer
@@ -178,7 +178,7 @@ export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => {
href={item.url}
className="text-rgray-11/90 bg-rgray-3 text-md absolute right-0 top-0 flex w-min translate-y-1/2 items-center justify-center gap-1 rounded-full px-5 py-1"
>
- <img src={item.image ?? '/brain.png'} className="h-4 w-4" />
+ <img src={item.image ?? "/brain.png"} className="h-4 w-4" />
{cleanUrl(item.url)}
</a>
</DrawerHeader>
@@ -188,16 +188,16 @@ export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => {
<Input
className=""
required
- value={item.title ?? ''}
- placeholder={item.title ?? 'Enter the title for the page'}
+ value={item.title ?? ""}
+ placeholder={item.title ?? "Enter the title for the page"}
/>
</div>
<div className="mt-5">
<Label>Additional Context</Label>
<Textarea
className=""
- value={item.content ?? ''}
- placeholder={'Enter additional context for this page'}
+ value={item.content ?? ""}
+ placeholder={"Enter additional context for this page"}
/>
</div>
<DrawerFooter className="flex flex-row-reverse items-center justify-end px-0 pt-5">
@@ -224,7 +224,7 @@ export const CategoryItem: React.FC<{ item: StoredContent }> = ({ item }) => {
onReorder={setItems}
as="div"
initial={{ height: 0 }}
- animate={{ height: 'auto' }}
+ animate={{ height: "auto" }}
exit={{
height: 0,
transition: {},
@@ -272,8 +272,8 @@ export const CategoryPage: React.FC<{
>
<div className="relative h-4 min-w-4">
<img
- src={item.image ?? '/brain.png'}
- alt={item.title ?? 'Untitiled website'}
+ src={item.image ?? "/brain.png"}
+ alt={item.title ?? "Untitiled website"}
className="z-1 h-4 w-4 transition-[transform,opacity] delay-150 duration-150"
/>
<ArrowUpRight
@@ -284,7 +284,7 @@ export const CategoryPage: React.FC<{
</div>
<span className="w-full truncate text-nowrap">
- {item.title ?? 'Untitled website'}
+ {item.title ?? "Untitled website"}
</span>
</a>
<button
diff --git a/apps/web/src/components/Sidebar/FilterCombobox.tsx b/apps/web/src/components/Sidebar/FilterCombobox.tsx
index 76b66db9..a8e3a1e5 100644
--- a/apps/web/src/components/Sidebar/FilterCombobox.tsx
+++ b/apps/web/src/components/Sidebar/FilterCombobox.tsx
@@ -1,10 +1,10 @@
-'use client';
+"use client";
-import * as React from 'react';
-import { Check, ChevronsUpDown } from 'lucide-react';
+import * as React from "react";
+import { Check, ChevronsUpDown } from "lucide-react";
-import { cn } from '@/lib/utils';
-import { Button } from '@/components/ui/button';
+import { cn } from "@/lib/utils";
+import { Button } from "@/components/ui/button";
import {
Command,
CommandEmpty,
@@ -12,28 +12,30 @@ import {
CommandInput,
CommandItem,
CommandList,
-} from '@/components/ui/command';
+} from "@/components/ui/command";
import {
Popover,
PopoverContent,
PopoverTrigger,
-} from '@/components/ui/popover';
-import { SpaceIcon } from '@/assets/Memories';
-import { AnimatePresence, LayoutGroup, motion } from 'framer-motion';
-import { useMemory } from '@/contexts/MemoryContext';
+} from "@/components/ui/popover";
+import { SpaceIcon } from "@/assets/Memories";
+import { AnimatePresence, LayoutGroup, motion } from "framer-motion";
+import { useMemory } from "@/contexts/MemoryContext";
export interface Props extends React.ButtonHTMLAttributes<HTMLButtonElement> {
- side?: 'top' | 'bottom';
- align?: 'end' | 'start' | 'center';
+ side?: "top" | "bottom";
+ align?: "end" | "start" | "center";
onClose?: () => void;
selectedSpaces: number[];
- setSelectedSpaces: (spaces: number[] | ((prev: number[]) => number[])) => void;
+ setSelectedSpaces: (
+ spaces: number[] | ((prev: number[]) => number[]),
+ ) => void;
}
export function FilterCombobox({
className,
- side = 'bottom',
- align = 'center',
+ side = "bottom",
+ align = "center",
onClose,
selectedSpaces,
setSelectedSpaces,
@@ -65,7 +67,7 @@ export function FilterCombobox({
<button
data-state-on={open}
className={cn(
- 'text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none',
+ "text-rgray-11/70 on:bg-rgray-3 focus-visible:ring-rgray-8 hover:bg-rgray-3 relative flex items-center justify-center gap-1 rounded-md px-3 py-1.5 ring-2 ring-transparent focus-visible:outline-none",
className,
)}
{...props}
@@ -129,7 +131,7 @@ export function FilterCombobox({
<Check
data-state-on={selectedSpaces.includes(space.id)}
className={cn(
- 'on:opacity-100 ml-auto h-4 w-4 opacity-0',
+ "on:opacity-100 ml-auto h-4 w-4 opacity-0",
)}
/>
</motion.div>
diff --git a/apps/web/src/components/Sidebar/index.tsx b/apps/web/src/components/Sidebar/index.tsx
index 830b0f05..568aa3dd 100644
--- a/apps/web/src/components/Sidebar/index.tsx
+++ b/apps/web/src/components/Sidebar/index.tsx
@@ -1,29 +1,35 @@
-'use client';
-import { MemoryIcon } from '../../assets/Memories';
-import { Trash2, User2 } from 'lucide-react';
-import React, { useEffect, useState } from 'react';
-import { MemoriesBar } from './MemoriesBar';
-import { AnimatePresence, motion } from 'framer-motion';
-import { Bin } from '@/assets/Bin';
-import { Avatar, AvatarFallback, AvatarImage } from '@radix-ui/react-avatar';
-import { useSession } from 'next-auth/react';
+"use client";
+import { MemoryIcon } from "../../assets/Memories";
+import { Trash2, User2 } from "lucide-react";
+import React, { useEffect, useState } from "react";
+import { MemoriesBar } from "./MemoriesBar";
+import { AnimatePresence, motion } from "framer-motion";
+import { Bin } from "@/assets/Bin";
+import { Avatar, AvatarFallback, AvatarImage } from "@radix-ui/react-avatar";
+import { useSession } from "next-auth/react";
+import MessagePoster from "@/app/MessagePoster";
+import Image from "next/image";
+import WordMark from "../WordMark";
export type MenuItem = {
icon: React.ReactNode | React.ReactNode[];
label: string;
content?: React.ReactNode;
+ labelDisplay?: React.ReactNode;
};
export default function Sidebar({
selectChange,
+ jwt,
}: {
selectChange?: (selectedItem: string | null) => void;
+ jwt: string;
}) {
const { data: session } = useSession();
const menuItemsTop: Array<MenuItem> = [
{
icon: <MemoryIcon className="h-10 w-10" />,
- label: 'Memories',
+ label: "Memories",
content: <MemoriesBar />,
},
];
@@ -31,7 +37,7 @@ export default function Sidebar({
const menuItemsBottom: Array<MenuItem> = [
{
icon: <Trash2 strokeWidth={1.3} className="h-6 w-6" />,
- label: 'Trash',
+ label: "Trash",
},
{
icon: (
@@ -47,12 +53,12 @@ export default function Sidebar({
<User2 strokeWidth={1.3} className="h-6 w-6" />
)}
<AvatarFallback>
- {session?.user?.name?.split(' ').map((n) => n[0])}{' '}
+ {session?.user?.name?.split(" ").map((n) => n[0])}{" "}
</AvatarFallback>
</Avatar>
</div>
),
- label: 'Profile',
+ label: "Profile",
},
];
@@ -70,22 +76,30 @@ export default function Sidebar({
return (
<>
<div className="relative hidden h-screen max-h-screen w-max flex-col items-center text-sm font-light md:flex">
- <div className="bg-rgray-2 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 ">
+ <div className="bg-rgray-3 border-r-rgray-6 relative z-[50] flex h-full w-full flex-col items-center justify-center border-r px-2 py-5 ">
+ <Image
+ className="mb-4 rounded-md"
+ src="/icons/logo_bw_without_bg.png"
+ alt="Smort logo"
+ width={50}
+ height={50}
+ />
+
+ <div className="bg-rgray-6 mb-8 h-[1px] w-full" />
+
<MenuItem
item={{
- label: 'Memories',
+ label: "Memories",
icon: <MemoryIcon className="h-10 w-10" />,
content: <MemoriesBar />,
}}
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
-
<div className="mt-auto" />
-
<MenuItem
item={{
- label: 'Trash',
+ label: "Trash",
icon: <Bin id="trash" className="z-[300] h-7 w-7" />,
}}
selectedItem={selectedItem}
@@ -94,7 +108,7 @@ export default function Sidebar({
/>
<MenuItem
item={{
- label: 'Profile',
+ label: "Profile",
icon: (
<div className="mb-2">
<Avatar>
@@ -108,7 +122,7 @@ export default function Sidebar({
<User2 strokeWidth={1.3} className="h-6 w-6" />
)}
<AvatarFallback>
- {session?.user?.name?.split(' ').map((n) => n[0])}{' '}
+ {session?.user?.name?.split(" ").map((n) => n[0])}{" "}
</AvatarFallback>
</Avatar>
</div>
@@ -117,6 +131,7 @@ export default function Sidebar({
selectedItem={selectedItem}
setSelectedItem={setSelectedItem}
/>
+ <MessagePoster jwt={jwt} />
</div>
<AnimatePresence>
{selectedItem && <SubSidebar>{Subbar}</SubSidebar>}
@@ -127,7 +142,7 @@ export default function Sidebar({
}
const MenuItem = ({
- item: { icon, label },
+ item: { icon, label, labelDisplay },
selectedItem,
setSelectedItem,
...props
@@ -143,18 +158,18 @@ const MenuItem = ({
{...props}
>
{icon}
- <span className="">{label}</span>
+ <span className="">{labelDisplay ?? label}</span>
</button>
);
export function SubSidebar({ children }: { children?: React.ReactNode }) {
return (
<motion.div
- initial={{ opacity: 0, x: '-100%' }}
+ initial={{ opacity: 0, x: "-100%" }}
animate={{ opacity: 1, x: 0 }}
exit={{
opacity: 0,
- x: '-100%',
+ x: "-100%",
transition: { delay: 0.2 },
}}
transition={{
diff --git a/apps/web/src/components/WordMark.tsx b/apps/web/src/components/WordMark.tsx
new file mode 100644
index 00000000..eb55647c
--- /dev/null
+++ b/apps/web/src/components/WordMark.tsx
@@ -0,0 +1,12 @@
+import { cn } from "@/lib/utils";
+import React from "react";
+
+function WordMark({ className }: { className?: string }) {
+ return (
+ <span className={cn(`text-xl font-bold tracking-tight ${className}`)}>
+ smort.
+ </span>
+ );
+}
+
+export default WordMark;
diff --git a/apps/web/src/components/ui/avatar.tsx b/apps/web/src/components/ui/avatar.tsx
index fb190df3..47795451 100644
--- a/apps/web/src/components/ui/avatar.tsx
+++ b/apps/web/src/components/ui/avatar.tsx
@@ -1,9 +1,9 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as AvatarPrimitive from "@radix-ui/react-avatar"
+import * as React from "react";
+import * as AvatarPrimitive from "@radix-ui/react-avatar";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const Avatar = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Root>,
@@ -13,12 +13,12 @@ const Avatar = React.forwardRef<
ref={ref}
className={cn(
"relative flex h-10 w-10 shrink-0 overflow-hidden rounded-full",
- className
+ className,
)}
{...props}
/>
-))
-Avatar.displayName = AvatarPrimitive.Root.displayName
+));
+Avatar.displayName = AvatarPrimitive.Root.displayName;
const AvatarImage = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Image>,
@@ -29,8 +29,8 @@ const AvatarImage = React.forwardRef<
className={cn("aspect-square h-full w-full", className)}
{...props}
/>
-))
-AvatarImage.displayName = AvatarPrimitive.Image.displayName
+));
+AvatarImage.displayName = AvatarPrimitive.Image.displayName;
const AvatarFallback = React.forwardRef<
React.ElementRef<typeof AvatarPrimitive.Fallback>,
@@ -40,11 +40,11 @@ const AvatarFallback = React.forwardRef<
ref={ref}
className={cn(
"flex h-full w-full items-center justify-center rounded-full bg-gray-100 dark:bg-gray-800",
- className
+ className,
)}
{...props}
/>
-))
-AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
+));
+AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName;
-export { Avatar, AvatarImage, AvatarFallback }
+export { Avatar, AvatarImage, AvatarFallback };
diff --git a/apps/web/src/components/ui/badge.tsx b/apps/web/src/components/ui/badge.tsx
index 1e21383f..40b15b91 100644
--- a/apps/web/src/components/ui/badge.tsx
+++ b/apps/web/src/components/ui/badge.tsx
@@ -1,7 +1,7 @@
-import * as React from "react"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const badgeVariants = cva(
"inline-flex items-center rounded-full border border-gray-200 px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-gray-950 focus:ring-offset-2 dark:border-gray-800 dark:focus:ring-gray-300",
@@ -20,8 +20,8 @@ const badgeVariants = cva(
defaultVariants: {
variant: "default",
},
- }
-)
+ },
+);
export interface BadgeProps
extends React.HTMLAttributes<HTMLDivElement>,
@@ -30,7 +30,7 @@ export interface BadgeProps
function Badge({ className, variant, ...props }: BadgeProps) {
return (
<div className={cn(badgeVariants({ variant }), className)} {...props} />
- )
+ );
}
-export { Badge, badgeVariants }
+export { Badge, badgeVariants };
diff --git a/apps/web/src/components/ui/button.tsx b/apps/web/src/components/ui/button.tsx
index b67d2657..24fa903e 100644
--- a/apps/web/src/components/ui/button.tsx
+++ b/apps/web/src/components/ui/button.tsx
@@ -1,8 +1,8 @@
-import * as React from "react"
-import { Slot } from "@radix-ui/react-slot"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import { Slot } from "@radix-ui/react-slot";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const buttonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-white transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-gray-950 focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 dark:ring-offset-gray-950 dark:focus-visible:ring-gray-300",
@@ -30,27 +30,27 @@ const buttonVariants = cva(
variant: "default",
size: "default",
},
- }
-)
+ },
+);
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof buttonVariants> {
- asChild?: boolean
+ asChild?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ className, variant, size, asChild = false, ...props }, ref) => {
- const Comp = asChild ? Slot : "button"
+ const Comp = asChild ? Slot : "button";
return (
<button
className={cn(buttonVariants({ variant, size, className }))}
ref={ref}
{...props}
/>
- )
- }
-)
-Button.displayName = "Button"
+ );
+ },
+);
+Button.displayName = "Button";
-export { Button, buttonVariants }
+export { Button, buttonVariants };
diff --git a/apps/web/src/components/ui/card.tsx b/apps/web/src/components/ui/card.tsx
index 65119a16..e98d500c 100644
--- a/apps/web/src/components/ui/card.tsx
+++ b/apps/web/src/components/ui/card.tsx
@@ -1,6 +1,6 @@
-import * as React from "react"
+import * as React from "react";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const Card = React.forwardRef<
HTMLDivElement,
@@ -10,12 +10,12 @@ const Card = React.forwardRef<
ref={ref}
className={cn(
"rounded-lg border border-gray-200 bg-white text-gray-950 shadow-sm dark:border-gray-800 dark:bg-gray-950 dark:text-gray-50",
- className
+ className,
)}
{...props}
/>
-))
-Card.displayName = "Card"
+));
+Card.displayName = "Card";
const CardHeader = React.forwardRef<
HTMLDivElement,
@@ -26,8 +26,8 @@ const CardHeader = React.forwardRef<
className={cn("flex flex-col space-y-1.5 p-6", className)}
{...props}
/>
-))
-CardHeader.displayName = "CardHeader"
+));
+CardHeader.displayName = "CardHeader";
const CardTitle = React.forwardRef<
HTMLParagraphElement,
@@ -37,12 +37,12 @@ const CardTitle = React.forwardRef<
ref={ref}
className={cn(
"text-2xl font-semibold leading-none tracking-tight",
- className
+ className,
)}
{...props}
/>
-))
-CardTitle.displayName = "CardTitle"
+));
+CardTitle.displayName = "CardTitle";
const CardDescription = React.forwardRef<
HTMLParagraphElement,
@@ -53,16 +53,16 @@ const CardDescription = React.forwardRef<
className={cn("text-sm text-gray-500 dark:text-gray-400", className)}
{...props}
/>
-))
-CardDescription.displayName = "CardDescription"
+));
+CardDescription.displayName = "CardDescription";
const CardContent = React.forwardRef<
HTMLDivElement,
React.HTMLAttributes<HTMLDivElement>
>(({ className, ...props }, ref) => (
<div ref={ref} className={cn("p-6 pt-0", className)} {...props} />
-))
-CardContent.displayName = "CardContent"
+));
+CardContent.displayName = "CardContent";
const CardFooter = React.forwardRef<
HTMLDivElement,
@@ -73,7 +73,14 @@ const CardFooter = React.forwardRef<
className={cn("flex items-center p-6 pt-0", className)}
{...props}
/>
-))
-CardFooter.displayName = "CardFooter"
+));
+CardFooter.displayName = "CardFooter";
-export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
+export {
+ Card,
+ CardHeader,
+ CardFooter,
+ CardTitle,
+ CardDescription,
+ CardContent,
+};
diff --git a/apps/web/src/components/ui/label.tsx b/apps/web/src/components/ui/label.tsx
index 53418217..84f8b0c7 100644
--- a/apps/web/src/components/ui/label.tsx
+++ b/apps/web/src/components/ui/label.tsx
@@ -1,14 +1,14 @@
-"use client"
+"use client";
-import * as React from "react"
-import * as LabelPrimitive from "@radix-ui/react-label"
-import { cva, type VariantProps } from "class-variance-authority"
+import * as React from "react";
+import * as LabelPrimitive from "@radix-ui/react-label";
+import { cva, type VariantProps } from "class-variance-authority";
-import { cn } from "@/lib/utils"
+import { cn } from "@/lib/utils";
const labelVariants = cva(
- "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
-)
+ "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70",
+);
const Label = React.forwardRef<
React.ElementRef<typeof LabelPrimitive.Root>,
@@ -20,7 +20,7 @@ const Label = React.forwardRef<
className={cn(labelVariants(), className)}
{...props}
/>
-))
-Label.displayName = LabelPrimitive.Root.displayName
+));
+Label.displayName = LabelPrimitive.Root.displayName;
-export { Label }
+export { Label };
diff --git a/apps/web/src/contexts/MemoryContext.tsx b/apps/web/src/contexts/MemoryContext.tsx
index 3727c464..eab1e4fe 100644
--- a/apps/web/src/contexts/MemoryContext.tsx
+++ b/apps/web/src/contexts/MemoryContext.tsx
@@ -1,6 +1,6 @@
-'use client';
-import React, { useCallback } from 'react';
-import { CollectedSpaces } from '../../types/memory';
+"use client";
+import React, { useCallback } from "react";
+import { CollectedSpaces } from "../../types/memory";
// temperory (will change)
export const MemoryContext = React.createContext<{
@@ -41,7 +41,7 @@ export const MemoryProvider: React.FC<
export const useMemory = () => {
const context = React.useContext(MemoryContext);
if (context === undefined) {
- throw new Error('useMemory must be used within a MemoryProvider');
+ throw new Error("useMemory must be used within a MemoryProvider");
}
return context;
};
diff --git a/apps/web/src/env.js b/apps/web/src/env.js
index 3d3085fa..2495d75b 100644
--- a/apps/web/src/env.js
+++ b/apps/web/src/env.js
@@ -1,7 +1,6 @@
import { createEnv } from "@t3-oss/env-nextjs";
import { z } from "zod";
-
export const env = createEnv({
/**
* Specify your server-side environment variables schema here. This way you can ensure the app
@@ -12,15 +11,13 @@ export const env = createEnv({
.enum(["development", "test", "production"])
.default("development"),
NEXTAUTH_SECRET:
- process.env.NODE_ENV === "production"
- ? z.string()
- : z.string(),
+ process.env.NODE_ENV === "production" ? z.string() : z.string(),
NEXTAUTH_URL: z.preprocess(
// This makes Vercel deployments not fail if you don't set NEXTAUTH_URL
// Since NextAuth.js automatically uses the VERCEL_URL if present.
(str) => process.env.VERCEL_URL ?? str,
// VERCEL_URL doesn't include `https` so it cant be validated as a URL
- process.env.VERCEL ? z.string() : z.string().url()
+ process.env.VERCEL ? z.string() : z.string().url(),
),
GOOGLE_CLIENT_ID: z.string(),
GOOGLE_CLIENT_SECRET: z.string(),
diff --git a/apps/web/src/lib/searchParams.ts b/apps/web/src/lib/searchParams.ts
index b435295d..aae3f4c7 100644
--- a/apps/web/src/lib/searchParams.ts
+++ b/apps/web/src/lib/searchParams.ts
@@ -1,12 +1,12 @@
import {
- createSearchParamsCache,
- parseAsInteger,
- parseAsString
- } from 'nuqs/server'
- // Note: import from 'nuqs/server' to avoid the "use client" directive
-
- export const searchParamsCache = createSearchParamsCache({
- // List your search param keys and associated parsers here:
- q: parseAsString.withDefault(''),
- maxResults: parseAsInteger.withDefault(10)
- }) \ No newline at end of file
+ createSearchParamsCache,
+ parseAsInteger,
+ parseAsString,
+} from "nuqs/server";
+// Note: import from 'nuqs/server' to avoid the "use client" directive
+
+export const searchParamsCache = createSearchParamsCache({
+ // List your search param keys and associated parsers here:
+ q: parseAsString.withDefault(""),
+ maxResults: parseAsInteger.withDefault(10),
+});
diff --git a/apps/web/src/server/auth.ts b/apps/web/src/server/auth.ts
index c32efe55..95edcf35 100644
--- a/apps/web/src/server/auth.ts
+++ b/apps/web/src/server/auth.ts
@@ -1,7 +1,7 @@
import { env } from "@/env";
import NextAuth from "next-auth";
import Google from "next-auth/providers/google";
-import { DrizzleAdapter } from "@auth/drizzle-adapter"
+import { DrizzleAdapter } from "@auth/drizzle-adapter";
import { db } from "./db";
export const {
@@ -15,9 +15,9 @@ export const {
...session,
user: {
...session.user,
- id: user.id
+ id: user.id,
},
- })
+ }),
},
adapter: DrizzleAdapter(db),
providers: [
diff --git a/apps/web/src/server/db/index.ts b/apps/web/src/server/db/index.ts
index 5aa87fc1..4d671bea 100644
--- a/apps/web/src/server/db/index.ts
+++ b/apps/web/src/server/db/index.ts
@@ -1,8 +1,5 @@
-import { drizzle } from 'drizzle-orm/d1';
+import { drizzle } from "drizzle-orm/d1";
import * as schema from "./schema";
-export const db = drizzle(
- process.env.DATABASE,
- { schema, logger: true }
-);
+export const db = drizzle(process.env.DATABASE, { schema, logger: true });
diff --git a/apps/web/src/server/db/schema.ts b/apps/web/src/server/db/schema.ts
index a80eb7cf..e0ddbdbc 100644
--- a/apps/web/src/server/db/schema.ts
+++ b/apps/web/src/server/db/schema.ts
@@ -6,7 +6,7 @@ import {
sqliteTableCreator,
text,
integer,
- unique
+ unique,
} from "drizzle-orm/sqlite-core";
export const createTable = sqliteTableCreator((name) => `${name}`);
@@ -78,7 +78,6 @@ export const verificationTokens = createTable(
}),
);
-
export const storedContent = createTable(
"storedContent",
{
@@ -103,8 +102,12 @@ export const storedContent = createTable(
export const contentToSpace = createTable(
"contentToSpace",
{
- contentId: integer("contentId").notNull().references(() => storedContent.id),
- spaceId: integer("spaceId").notNull().references(() => space.id),
+ contentId: integer("contentId")
+ .notNull()
+ .references(() => storedContent.id),
+ spaceId: integer("spaceId")
+ .notNull()
+ .references(() => space.id),
},
(cts) => ({
compoundKey: primaryKey({ columns: [cts.contentId, cts.spaceId] }),
@@ -115,7 +118,7 @@ export const space = createTable(
"space",
{
id: integer("id").notNull().primaryKey({ autoIncrement: true }),
- name: text('name').notNull().default('all'),
+ name: text("name").notNull().default("all"),
user: text("user", { length: 255 }).references(() => users.id),
},
(space) => ({
@@ -124,4 +127,4 @@ export const space = createTable(
}),
);
-export type StoredContent = Omit<typeof storedContent.$inferSelect, 'user'> \ No newline at end of file
+export type StoredContent = Omit<typeof storedContent.$inferSelect, "user">;
diff --git a/apps/web/src/server/helpers.ts b/apps/web/src/server/helpers.ts
index 1f6cf977..519e4b17 100644
--- a/apps/web/src/server/helpers.ts
+++ b/apps/web/src/server/helpers.ts
@@ -1,34 +1,34 @@
export async function getMetaData(url: string) {
- const response = await fetch(url);
- const html = await response.text();
-
- // Extract the base URL
- const baseUrl = new URL(url).origin;
-
- // Extract title
- const titleMatch = html.match(/<title>(.*?)<\/title>/);
- const title = titleMatch ? titleMatch[1] : 'Title not found';
-
- // Extract meta description
- const descriptionMatch = html.match(
- /<meta name="description" content="(.*?)"\s*\/?>/,
- );
- const description = descriptionMatch
- ? descriptionMatch[1]
- : 'Description not found';
-
- // Extract Open Graph image
- const imageMatch = html.match(
- /<meta property="og:image" content="(.*?)"\s*\/?>/,
- );
- const image = imageMatch ? imageMatch[1] : 'Image not found';
-
- // Prepare the metadata object
- const metadata = {
- title,
- description,
- image,
- baseUrl,
- };
- return metadata;
- } \ No newline at end of file
+ const response = await fetch(url);
+ const html = await response.text();
+
+ // Extract the base URL
+ const baseUrl = new URL(url).origin;
+
+ // Extract title
+ const titleMatch = html.match(/<title>(.*?)<\/title>/);
+ const title = titleMatch ? titleMatch[1] : "Title not found";
+
+ // Extract meta description
+ const descriptionMatch = html.match(
+ /<meta name="description" content="(.*?)"\s*\/?>/,
+ );
+ const description = descriptionMatch
+ ? descriptionMatch[1]
+ : "Description not found";
+
+ // Extract Open Graph image
+ const imageMatch = html.match(
+ /<meta property="og:image" content="(.*?)"\s*\/?>/,
+ );
+ const image = imageMatch ? imageMatch[1] : "Image not found";
+
+ // Prepare the metadata object
+ const metadata = {
+ title,
+ description,
+ image,
+ baseUrl,
+ };
+ return metadata;
+}