From c12ecfc4316a6f37d2d07c57e4dfefa231783c0d Mon Sep 17 00:00:00 2001 From: Dhravya Date: Sat, 25 May 2024 23:38:48 -0500 Subject: [PATCH] brought all the APIs back --- apps/web/app/api/chat/route.ts | 73 +++++++++++ apps/web/app/api/getCount/route.ts | 47 +++++++ apps/web/app/api/me/route.ts | 47 +++++++ apps/web/app/api/spaces/route.ts | 29 +++++ apps/web/app/api/store/route.ts | 121 ++++++++++++++++++ apps/web/app/helpers/lib/get-metadata.ts | 40 ++++++ apps/web/app/upload/uploaded-files-card.tsx | 8 +- apps/web/env.d.ts | 3 +- apps/web/package.json | 2 +- apps/web/tsconfig.json | 5 +- package.json | 2 + packages/shared-types/index.ts | 10 +- packages/shared-types/package.json | 14 +- packages/ui/hooks/use-upload-file.tsx | 73 ----------- .../ui/{ => src}/hooks/use-callback-ref.ts | 0 .../{ => src}/hooks/use-controllable-state.ts | 2 +- packages/ui/src/icons/add.svg | 3 + packages/ui/src/icons/arrowright.svg | 3 + packages/ui/src/icons/chat.svg | 3 + packages/ui/src/icons/explore.svg | 3 + packages/ui/src/icons/history.svg | 3 + packages/ui/src/icons/index.ts | 17 +++ packages/ui/src/icons/memories.svg | 3 + packages/ui/src/icons/select.svg | 4 + packages/ui/src/shadcn/divider.tsx | 7 + packages/ui/src/shadcn/form.tsx | 2 +- packages/ui/src/ui.d.ts | 4 + 27 files changed, 430 insertions(+), 98 deletions(-) create mode 100644 apps/web/app/api/chat/route.ts create mode 100644 apps/web/app/api/getCount/route.ts create mode 100644 apps/web/app/api/me/route.ts create mode 100644 apps/web/app/api/spaces/route.ts create mode 100644 apps/web/app/api/store/route.ts create mode 100644 apps/web/app/helpers/lib/get-metadata.ts delete mode 100644 packages/ui/hooks/use-upload-file.tsx rename packages/ui/{ => src}/hooks/use-callback-ref.ts (100%) rename packages/ui/{ => src}/hooks/use-controllable-state.ts (96%) create mode 100644 packages/ui/src/icons/add.svg create mode 100644 packages/ui/src/icons/arrowright.svg create mode 100644 packages/ui/src/icons/chat.svg create mode 100644 packages/ui/src/icons/explore.svg create mode 100644 packages/ui/src/icons/history.svg create mode 100644 packages/ui/src/icons/index.ts create mode 100644 packages/ui/src/icons/memories.svg create mode 100644 packages/ui/src/icons/select.svg create mode 100644 packages/ui/src/shadcn/divider.tsx create mode 100644 packages/ui/src/ui.d.ts diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts new file mode 100644 index 00000000..34099848 --- /dev/null +++ b/apps/web/app/api/chat/route.ts @@ -0,0 +1,73 @@ +import { type NextRequest } from "next/server"; +import { ChatHistory } from "@repo/shared-types"; +import { ensureAuth } from "../ensureAuth"; + +export const runtime = "edge"; + +export async function POST(req: NextRequest) { + const session = await ensureAuth(req); + + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + + if (!process.env.BACKEND_SECURITY_KEY) { + return new Response("Missing BACKEND_SECURITY_KEY", { status: 500 }); + } + + 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, + }); + } + + try { + 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": process.env.BACKEND_SECURITY_KEY!, + }, + method: "POST", + body: JSON.stringify({ + chatHistory: chatHistory.chatHistory ?? [], + }), + }, + ); + + console.log("sourcesOnly", sourcesOnly); + + if (sourcesOnly == "true") { + const data = await resp.json(); + console.log("data", data); + return new Response(JSON.stringify(data), { status: 200 }); + } + + 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 }); + } catch {} +} diff --git a/apps/web/app/api/getCount/route.ts b/apps/web/app/api/getCount/route.ts new file mode 100644 index 00000000..f760c145 --- /dev/null +++ b/apps/web/app/api/getCount/route.ts @@ -0,0 +1,47 @@ +import { db } from "@/app/helpers/server/db"; +import { and, eq, ne, sql } from "drizzle-orm"; +import { sessions, storedContent, users } from "@/app/helpers/server/db/schema"; +import { type NextRequest, NextResponse } from "next/server"; +import { ensureAuth } from "../ensureAuth"; + +export const runtime = "edge"; + +export async function GET(req: NextRequest) { + const session = await ensureAuth(req); + + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + + const tweetsCount = await db + .select({ + count: sql`count(*)`.mapWith(Number), + }) + .from(storedContent) + .where( + and( + eq(storedContent.user, session.user.id), + eq(storedContent.type, "twitter-bookmark"), + ), + ); + + const pageCount = await db + .select({ + count: sql`count(*)`.mapWith(Number), + }) + .from(storedContent) + .where( + and( + eq(storedContent.user, session.user.id), + ne(storedContent.type, "twitter-bookmark"), + ), + ); + + return NextResponse.json({ + tweetsCount: tweetsCount[0]!.count, + tweetsLimit: 1000, + pageCount: pageCount[0]!.count, + pageLimit: 100, + user: session.user.email, + }); +} diff --git a/apps/web/app/api/me/route.ts b/apps/web/app/api/me/route.ts new file mode 100644 index 00000000..20b6aece --- /dev/null +++ b/apps/web/app/api/me/route.ts @@ -0,0 +1,47 @@ +import { db } from "@/app/helpers/server/db"; +import { eq } from "drizzle-orm"; +import { sessions, users } from "@/app/helpers/server/db/schema"; +import { type NextRequest, NextResponse } from "next/server"; + +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 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 }, + ); + } + + 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 }, + ); + } + + return new Response( + JSON.stringify({ + message: "OK", + data: { session: session[0], user: user[0] }, + }), + { status: 200 }, + ); +} diff --git a/apps/web/app/api/spaces/route.ts b/apps/web/app/api/spaces/route.ts new file mode 100644 index 00000000..c46b02fc --- /dev/null +++ b/apps/web/app/api/spaces/route.ts @@ -0,0 +1,29 @@ +import { db } from "@/app/helpers/server/db"; +import { sessions, space, users } from "@/app/helpers/server/db/schema"; +import { eq } from "drizzle-orm"; +import { NextRequest, NextResponse } from "next/server"; +import { ensureAuth } from "../ensureAuth"; + +export const runtime = "edge"; + +export async function GET(req: NextRequest) { + const session = await ensureAuth(req); + + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + + const spaces = await db + .select() + .from(space) + .where(eq(space.user, session.user.id)) + .all(); + + return NextResponse.json( + { + message: "OK", + data: spaces, + }, + { status: 200 }, + ); +} diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts new file mode 100644 index 00000000..f96f90cf --- /dev/null +++ b/apps/web/app/api/store/route.ts @@ -0,0 +1,121 @@ +import { db } from "@/app/helpers/server/db"; +import { and, eq, sql, inArray } from "drizzle-orm"; +import { + contentToSpace, + sessions, + storedContent, + users, + space, +} from "@/app/helpers/server/db/schema"; +import { type NextRequest, NextResponse } from "next/server"; +import { getMetaData } from "@/app/helpers/lib/get-metadata"; +import { ensureAuth } from "../ensureAuth"; + +export const runtime = "edge"; + +export async function POST(req: NextRequest) { + const session = await ensureAuth(req); + + if (!session) { + return new Response("Unauthorized", { status: 401 }); + } + + const data = (await req.json()) as { + pageContent: string; + url: string; + spaces?: string[]; + }; + + const metadata = await getMetaData(data.url); + let storeToSpaces = data.spaces; + + if (!storeToSpaces) { + storeToSpaces = []; + } + + const count = await db + .select({ + count: sql`count(*)`.mapWith(Number), + }) + .from(storedContent) + .where( + and( + eq(storedContent.user, session.user.id), + eq(storedContent.type, "page"), + ), + ); + + if (count[0]!.count > 100) { + return NextResponse.json( + { message: "Error", error: "Limit exceeded" }, + { status: 499 }, + ); + } + + const rep = 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, + }) + .returning({ id: storedContent.id }); + + const id = rep[0]?.id; + + if (!id) { + return NextResponse.json( + { message: "Error", error: "Error in CF function" }, + { status: 500 }, + ); + } + + if (storeToSpaces.length > 0) { + const spaceData = await db + .select() + .from(space) + .where( + and( + inArray(space.name, storeToSpaces ?? []), + eq(space.user, session.user.id), + ), + ) + .all(); + + await Promise.all([ + spaceData.forEach(async (space) => { + await db + .insert(contentToSpace) + .values({ contentId: id, spaceId: space.id }); + }), + ]); + } + + const res = (await Promise.race([ + fetch("https://cf-ai-backend.dhravya.workers.dev/add", { + method: "POST", + headers: { + "X-Custom-Auth-Key": process.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) { + console.log(res.status, res.statusText); + 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/app/helpers/lib/get-metadata.ts b/apps/web/app/helpers/lib/get-metadata.ts new file mode 100644 index 00000000..4609e49b --- /dev/null +++ b/apps/web/app/helpers/lib/get-metadata.ts @@ -0,0 +1,40 @@ +"use server"; +import * as cheerio from "cheerio"; + +// TODO: THIS SHOULD PROBABLY ALSO FETCH THE OG-IMAGE +export async function getMetaData(url: string) { + const response = await fetch(url); + const html = await response.text(); + + const $ = cheerio.load(html); + + // Extract the base URL + const baseUrl = new URL(url).origin; + + // Extract title + const title = $("title").text().trim(); + + const description = $("meta[name=description]").attr("content") ?? ""; + + const _favicon = + $("link[rel=icon]").attr("href") ?? "https://supermemory.dhr.wtf/web.svg"; + + let favicon = + _favicon.trim().length > 0 + ? _favicon.trim() + : "https://supermemory.dhr.wtf/web.svg"; + if (favicon.startsWith("/")) { + favicon = baseUrl + favicon; + } else if (favicon.startsWith("./")) { + favicon = baseUrl + favicon.slice(1); + } + + // Prepare the metadata object + const metadata = { + title, + description, + image: favicon, + baseUrl, + }; + return metadata; +} diff --git a/apps/web/app/upload/uploaded-files-card.tsx b/apps/web/app/upload/uploaded-files-card.tsx index fc9b7d5b..869663b5 100644 --- a/apps/web/app/upload/uploaded-files-card.tsx +++ b/apps/web/app/upload/uploaded-files-card.tsx @@ -1,5 +1,5 @@ import Image from "next/image"; -import type { UploadedFile } from "@repo/shared-types"; +// import type { UploadedFile } from '@repo/shared-types'; import { Card, @@ -7,11 +7,11 @@ import { CardDescription, CardHeader, CardTitle, -} from "@repo/ui/src/card"; -import { ScrollArea, ScrollBar } from "@repo/ui/src/scroll-area"; +} from "@repo/ui/src/shadcn/card"; +import { ScrollArea, ScrollBar } from "@repo/ui/src/shadcn/scroll-area"; interface UploadedFilesCardProps { - uploadedFiles: UploadedFile[]; + uploadedFiles: any[]; } import { ImageIcon } from "@radix-ui/react-icons"; diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts index d74dcdf3..161177a1 100644 --- a/apps/web/env.d.ts +++ b/apps/web/env.d.ts @@ -1,4 +1,4 @@ -// Generated by Wrangler on Sat May 25 2024 17:07:13 GMT-0500 (Central Daylight Time) +// Generated by Wrangler on Sat May 25 2024 23:32:58 GMT-0500 (Central Daylight Time) // by running `wrangler types --env-interface CloudflareEnv env.d.ts` interface CloudflareEnv { @@ -9,6 +9,7 @@ interface CloudflareEnv { R2_ACCESS_ID: string; R2_SECRET_KEY: string; R2_BUCKET_NAME: string; + BACKEND_SECURITY_KEY: string; STORAGE: R2Bucket; DATABASE: D1Database; } diff --git a/apps/web/package.json b/apps/web/package.json index 70ad3b46..adca3031 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,5 +1,5 @@ { - "name": "web", + "name": "@repo/web", "version": "1.0.0", "private": true, "scripts": { diff --git a/apps/web/tsconfig.json b/apps/web/tsconfig.json index e5590d6b..ee2fb650 100644 --- a/apps/web/tsconfig.json +++ b/apps/web/tsconfig.json @@ -5,7 +5,10 @@ { "name": "next" } - ] + ], + "paths": { + "@/*": ["./*"] + } }, "include": [ "cf-env.d.ts", diff --git a/package.json b/package.json index 32f0b528..d169b935 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "@repo/shared-types": "*", "@repo/tailwind-config": "*", "@repo/typescript-config": "*", + "@repo/ui": "*", "@tailwindcss/typography": "^0.5.13", "autoprefixer": "^10.4.19", "drizzle-kit": "^0.21.2", @@ -58,6 +59,7 @@ "@radix-ui/react-toast": "^1.1.5", "@types/readline-sync": "^1.4.8", "ai": "^3.1.14", + "cheerio": "^1.0.0-rc.12", "compromise": "^14.13.0", "drizzle-orm": "^0.30.10", "framer-motion": "^11.2.6", diff --git a/packages/shared-types/index.ts b/packages/shared-types/index.ts index 0ccc2ef7..bf4a56da 100644 --- a/packages/shared-types/index.ts +++ b/packages/shared-types/index.ts @@ -1,3 +1,7 @@ -import { type ClientUploadedFileData } from "uploadthing/types"; - -export interface UploadedFile extends ClientUploadedFileData {} +export type ChatHistory = { + question: string; + answer: { + parts: { text: string }[]; + sources: { isNote: boolean; source: string }[]; + }; +}; diff --git a/packages/shared-types/package.json b/packages/shared-types/package.json index f4eaa5bd..18e6049e 100644 --- a/packages/shared-types/package.json +++ b/packages/shared-types/package.json @@ -1,15 +1,3 @@ { - "name": "@repo/shared-types", - "version": "0.0.0", - "private": true, - "type": "module", - "license": "MIT", - "publishConfig": { - "access": "public" - }, - "dependencies": { - "@repo/eslint-config": "*", - "@repo/typescript-config": "*", - "@repo/tailwind-config": "*" - } + "name": "@repo/shared-types" } diff --git a/packages/ui/hooks/use-upload-file.tsx b/packages/ui/hooks/use-upload-file.tsx deleted file mode 100644 index c81d735b..00000000 --- a/packages/ui/hooks/use-upload-file.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import { UploadedFile } from "@repo/shared-types"; -import { useState } from "react"; - -export function useUploadFile(key: string, options?: T) { - const [isUploading, setIsUploading] = useState(false); - const [uploadedFiles, setUploadedFiles] = useState([]); - - const uploadFiles = async (image: File[]) => { - setIsUploading(true); - await Promise.all( - image.map(async (file) => { - const fileName = - file.name.split(".")[0]! + Date.now() + "." + file.name.split(".")[1]; - - const formData = new FormData(); - formData.append("data", file); - - const response = await fetch(`/api/upload_image?filename=${fileName}`, { - method: "PUT", - body: formData, - }); - - if (response.status !== 200) { - throw new Error(response.statusText); - } - - const resp = (await response.json()) as { url: string }; - const url = resp.url; - - const response2 = await fetch(url, { - method: "PUT", - body: file, - headers: { - "Content-Type": file.type, - }, - }); - - if (response2.status !== 200) { - throw new Error(response2.statusText); - } - - // For showing on the client - const uri = URL.createObjectURL(file); - - setUploadedFiles((prev) => [ - ...prev, - { - name: fileName, - url: uri, - size: file.size, - type: file.type, - customId: null, - key: fileName, - serverData: null, - }, - ]); - - return { - url, - filename: fileName, - }; - }), - ); - - setIsUploading(false); - }; - - return { - uploadFiles, - uploadedFiles, - isUploading, - }; -} diff --git a/packages/ui/hooks/use-callback-ref.ts b/packages/ui/src/hooks/use-callback-ref.ts similarity index 100% rename from packages/ui/hooks/use-callback-ref.ts rename to packages/ui/src/hooks/use-callback-ref.ts diff --git a/packages/ui/hooks/use-controllable-state.ts b/packages/ui/src/hooks/use-controllable-state.ts similarity index 96% rename from packages/ui/hooks/use-controllable-state.ts rename to packages/ui/src/hooks/use-controllable-state.ts index 5ffc90f6..7cf20f89 100644 --- a/packages/ui/hooks/use-controllable-state.ts +++ b/packages/ui/src/hooks/use-controllable-state.ts @@ -1,6 +1,6 @@ import * as React from "react"; -import { useCallbackRef } from "@repo/ui/hooks/use-callback-ref"; +import { useCallbackRef } from "@repo/ui/src/hooks/use-callback-ref.js"; /** * @see https://github.com/radix-ui/primitives/blob/main/packages/react/use-controllable-state/src/useControllableState.tsx diff --git a/packages/ui/src/icons/add.svg b/packages/ui/src/icons/add.svg new file mode 100644 index 00000000..1c6d87f6 --- /dev/null +++ b/packages/ui/src/icons/add.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/icons/arrowright.svg b/packages/ui/src/icons/arrowright.svg new file mode 100644 index 00000000..d794b094 --- /dev/null +++ b/packages/ui/src/icons/arrowright.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/icons/chat.svg b/packages/ui/src/icons/chat.svg new file mode 100644 index 00000000..7fe96c16 --- /dev/null +++ b/packages/ui/src/icons/chat.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/icons/explore.svg b/packages/ui/src/icons/explore.svg new file mode 100644 index 00000000..d6332612 --- /dev/null +++ b/packages/ui/src/icons/explore.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/icons/history.svg b/packages/ui/src/icons/history.svg new file mode 100644 index 00000000..7766c75d --- /dev/null +++ b/packages/ui/src/icons/history.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/icons/index.ts b/packages/ui/src/icons/index.ts new file mode 100644 index 00000000..6c528aba --- /dev/null +++ b/packages/ui/src/icons/index.ts @@ -0,0 +1,17 @@ +import AddIcon from "./add.svg"; +import ChatIcon from "./chat.svg"; +import HistoryIcon from "./history.svg"; +import ExploreIcon from "./explore.svg"; +import MemoriesIcon from "./memories.svg"; +import ArrowRightIcon from "./arrowRight.svg"; +import SelectIcon from "./select.svg"; + +export { + AddIcon, + ChatIcon, + HistoryIcon, + ExploreIcon, + MemoriesIcon, + ArrowRightIcon, + SelectIcon, +}; diff --git a/packages/ui/src/icons/memories.svg b/packages/ui/src/icons/memories.svg new file mode 100644 index 00000000..27784aac --- /dev/null +++ b/packages/ui/src/icons/memories.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/packages/ui/src/icons/select.svg b/packages/ui/src/icons/select.svg new file mode 100644 index 00000000..0b019c01 --- /dev/null +++ b/packages/ui/src/icons/select.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/packages/ui/src/shadcn/divider.tsx b/packages/ui/src/shadcn/divider.tsx new file mode 100644 index 00000000..34439866 --- /dev/null +++ b/packages/ui/src/shadcn/divider.tsx @@ -0,0 +1,7 @@ +import { cn } from "@repo/ui/lib/utils"; + +function Divider({ className }: { className?: string }) { + return
; +} + +export default Divider; diff --git a/packages/ui/src/shadcn/form.tsx b/packages/ui/src/shadcn/form.tsx index 9de2d23e..452a6e30 100644 --- a/packages/ui/src/shadcn/form.tsx +++ b/packages/ui/src/shadcn/form.tsx @@ -11,7 +11,7 @@ import { } from "react-hook-form"; import { cn } from "@repo/ui/lib/utils"; -import { Label } from "@repo/ui/shadcn/label"; +import { Label } from "@repo/ui/src/shadcn/label"; const Form = FormProvider; diff --git a/packages/ui/src/ui.d.ts b/packages/ui/src/ui.d.ts new file mode 100644 index 00000000..1a3dd3c2 --- /dev/null +++ b/packages/ui/src/ui.d.ts @@ -0,0 +1,4 @@ +declare module "*.svg" { + const content: any; + export default content; +}