aboutsummaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorCodeTorso <[email protected]>2024-06-20 08:38:21 -0600
committerGitHub <[email protected]>2024-06-20 08:38:21 -0600
commitaf90b960af7a8e6debc059f8ca67af878f0f409a (patch)
tree8097644103d6f49b5b247ddada12e78132167f93 /apps/web
parentadd: animated query input (diff)
parentadded multi-turn conversations (diff)
downloadsupermemory-af90b960af7a8e6debc059f8ca67af878f0f409a.tar.xz
supermemory-af90b960af7a8e6debc059f8ca67af878f0f409a.zip
Merge branch 'codetorso' into kartik
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/(auth)/auth-buttons.tsx2
-rw-r--r--apps/web/app/(auth)/signin/page.tsx2
-rw-r--r--apps/web/app/(canvas)/canvas.tsx79
-rw-r--r--apps/web/app/(canvas)/canvas/page.tsx2
-rw-r--r--apps/web/app/(canvas)/dropComponent.tsx76
-rw-r--r--apps/web/app/(canvas)/enabledComp.tsx2
-rw-r--r--apps/web/app/(canvas)/lib/context.tsx11
-rw-r--r--apps/web/app/(canvas)/lib/createEmbeds.ts52
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx189
-rw-r--r--apps/web/app/(dash)/dynamicisland.tsx51
-rw-r--r--apps/web/app/(dash)/home/page.tsx39
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx96
-rw-r--r--apps/web/app/(dash)/layout.tsx2
-rw-r--r--apps/web/app/(dash)/memories/page.tsx121
-rw-r--r--apps/web/app/(editor)/components/aigenerate.tsx107
-rw-r--r--apps/web/app/(editor)/editor.tsx5
-rw-r--r--apps/web/app/(landing)/Cta.tsx2
-rw-r--r--apps/web/app/(landing)/page.tsx2
-rw-r--r--apps/web/app/actions/doers.ts231
-rw-r--r--apps/web/app/actions/fetchers.ts125
-rw-r--r--apps/web/app/actions/types.ts1
-rw-r--r--apps/web/app/api/[...nextauth]/route.ts2
-rw-r--r--apps/web/app/api/chat/route.ts2
-rw-r--r--apps/web/app/api/editorai/route.ts30
-rw-r--r--apps/web/app/api/ensureAuth.ts4
-rw-r--r--apps/web/app/api/getCount/route.ts8
-rw-r--r--apps/web/app/api/me/route.ts4
-rw-r--r--apps/web/app/api/spaces/route.ts4
-rw-r--r--apps/web/app/api/store/route.ts31
-rw-r--r--apps/web/app/api/unfirlsite/route.ts134
-rw-r--r--apps/web/app/ref/page.tsx8
-rw-r--r--apps/web/cf-env.d.ts13
-rw-r--r--apps/web/env.d.ts8
-rw-r--r--apps/web/lib/constants.ts (renamed from apps/web/app/helpers/constants.ts)6
-rw-r--r--apps/web/lib/get-metadata.ts (renamed from apps/web/app/helpers/lib/get-metadata.ts)0
-rw-r--r--apps/web/lib/get-theme-button.tsx (renamed from apps/web/app/helpers/lib/get-theme-button.tsx)0
-rw-r--r--apps/web/lib/handle-errors.ts (renamed from apps/web/app/helpers/lib/handle-errors.ts)0
-rw-r--r--apps/web/lib/searchParams.ts (renamed from apps/web/app/helpers/lib/searchParams.ts)0
-rw-r--r--apps/web/server/auth.ts (renamed from apps/web/app/helpers/server/auth.ts)0
-rw-r--r--apps/web/server/db/index.ts (renamed from apps/web/app/helpers/server/db/index.ts)0
-rw-r--r--apps/web/server/db/schema.ts (renamed from apps/web/app/helpers/server/db/schema.ts)8
41 files changed, 1196 insertions, 263 deletions
diff --git a/apps/web/app/(auth)/auth-buttons.tsx b/apps/web/app/(auth)/auth-buttons.tsx
index 0e99213e..5b0ad06e 100644
--- a/apps/web/app/(auth)/auth-buttons.tsx
+++ b/apps/web/app/(auth)/auth-buttons.tsx
@@ -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 (
diff --git a/apps/web/app/(auth)/signin/page.tsx b/apps/web/app/(auth)/signin/page.tsx
index ba84a94a..d7bad8da 100644
--- a/apps/web/app/(auth)/signin/page.tsx
+++ b/apps/web/app/(auth)/signin/page.tsx
@@ -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";
diff --git a/apps/web/app/(canvas)/canvas.tsx b/apps/web/app/(canvas)/canvas.tsx
index 9ec57d6d..498ab1eb 100644
--- a/apps/web/app/(canvas)/canvas.tsx
+++ b/apps/web/app/(canvas)/canvas.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo, useState } from "react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw";
import { createAssetFromUrl } from "./lib/createAssetUrl";
import "tldraw/tldraw.css";
@@ -7,10 +7,53 @@ import { twitterCardUtil } from "./twitterCard";
import createEmbedsFromUrl from "./lib/createEmbeds";
import { loadRemoteSnapshot } from "./lib/loadSnap";
import { SaveStatus } from "./savesnap";
-import { getAssetUrls } from '@tldraw/assets/selfHosted'
-import { memo } from 'react';
+import { getAssetUrls } from "@tldraw/assets/selfHosted";
+import { memo } from "react";
+import DragContext from "./lib/context";
+import DropZone from "./dropComponent";
-export const Canvas = memo(()=>{
+export const Canvas = memo(() => {
+ const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
+ const Dragref = useRef<HTMLDivElement | null>(null)
+
+ const handleDragOver = (event: any) => {
+ event.preventDefault();
+ setIsDraggingOver(true);
+ console.log("entere")
+ };
+
+ const handleDragLeave = () => {
+ setIsDraggingOver(false);
+ console.log("leaver")
+ };
+
+ useEffect(() => {
+ const divElement = Dragref.current;
+ if (divElement) {
+ divElement.addEventListener('dragover', handleDragOver);
+ divElement.addEventListener('dragleave', handleDragLeave);
+ }
+ return () => {
+ if (divElement) {
+ divElement.removeEventListener('dragover', handleDragOver);
+ divElement.removeEventListener('dragleave', handleDragLeave);
+ }
+ };
+ }, []);
+
+ return (
+ <DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}>
+ <div
+ ref={Dragref}
+ className="w-full h-full"
+ >
+ <TldrawComponent />
+ </div>
+ </DragContext.Provider>
+ );
+});
+
+const TldrawComponent =memo(() => {
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: "loading",
});
@@ -38,18 +81,22 @@ export const Canvas = memo(()=>{
setUserPreferences({ id: "supermemory", isDarkMode: true });
- const assetUrls = getAssetUrls()
+ const assetUrls = getAssetUrls();
return (
- <Tldraw
- assetUrls={assetUrls}
- components={components}
- store={storeWithStatus}
- shapeUtils={[twitterCardUtil]}
- onMount={handleMount}
- >
- <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
- <SaveStatus />
- </div>
- </Tldraw>
+ <div className="w-full h-full">
+ <Tldraw
+ className="relative"
+ assetUrls={assetUrls}
+ components={components}
+ store={storeWithStatus}
+ shapeUtils={[twitterCardUtil]}
+ onMount={handleMount}
+ >
+ <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
+ <SaveStatus />
+ </div>
+ <DropZone />
+ </Tldraw>
+ </div>
);
})
diff --git a/apps/web/app/(canvas)/canvas/page.tsx b/apps/web/app/(canvas)/canvas/page.tsx
index 7abfa583..366a4481 100644
--- a/apps/web/app/(canvas)/canvas/page.tsx
+++ b/apps/web/app/(canvas)/canvas/page.tsx
@@ -18,7 +18,7 @@ function page() {
const [fullScreen, setFullScreen] = useState(false);
return (
- <div className={`h-screen w-full ${ !fullScreen && "px-4 py-6"} transition-all`}>
+ <div className={`h-screen w-full ${ !fullScreen ? "px-4 py-6": "bg-[#1F2428]"} transition-all`}>
<div>
<PanelGroup className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`} direction="horizontal">
<Panel onExpand={()=> {setTimeout(()=> setFullScreen(false), 50)}} onCollapse={()=> {setTimeout(()=> setFullScreen(true), 50)}} defaultSize={30} collapsible={true} minSize={22}>
diff --git a/apps/web/app/(canvas)/dropComponent.tsx b/apps/web/app/(canvas)/dropComponent.tsx
new file mode 100644
index 00000000..03a32358
--- /dev/null
+++ b/apps/web/app/(canvas)/dropComponent.tsx
@@ -0,0 +1,76 @@
+import React, { useRef, useCallback, useEffect, useContext } from "react";
+import { useEditor } from "tldraw";
+import DragContext, { DragContextType } from "./lib/context";
+import { handleExternalDroppedContent } from "./lib/createEmbeds";
+
+const stripHtmlTags = (html: string): string => {
+ const div = document.createElement("div");
+ div.innerHTML = html;
+ return div.textContent || div.innerText || "";
+};
+
+const useDrag = (): DragContextType => {
+ const context = useContext(DragContext);
+ if (!context) {
+ throw new Error('useCounter must be used within a CounterProvider');
+ }
+ return context;
+};
+
+
+function DropZone() {
+ const dropRef = useRef<HTMLDivElement | null>(null);
+ const {isDraggingOver, setIsDraggingOver} = useDrag();
+
+ const editor = useEditor();
+
+ const handleDrop = useCallback((event: React.DragEvent<HTMLDivElement>) => {
+ event.preventDefault();
+ setIsDraggingOver(false);
+ const dt = event.dataTransfer;
+ const items = dt.items;
+
+ for (let i = 0; i < items.length; i++) {
+ if (items[i]!.kind === "file" && items[i]!.type.startsWith("image/")) {
+ const file = items[i]!.getAsFile();
+ if (file) {
+ const reader = new FileReader();
+ reader.onload = (e) => {
+ if (e.target) {
+ // setDroppedImage(e.target.result as string);
+ }
+ };
+ reader.readAsDataURL(file);
+ }
+ } else if (items[i]!.kind === "string") {
+ items[i]!.getAsString((data) => {
+ const cleanText = stripHtmlTags(data);
+ handleExternalDroppedContent({editor,text:cleanText})
+ });
+ }
+ }
+ }, []);
+
+ useEffect(() => {
+ const divElement = dropRef.current;
+ if (divElement) {
+ // @ts-ignore
+ divElement.addEventListener("drop", handleDrop);
+ }
+ return () => {
+ if (divElement) {
+ // @ts-ignore
+ divElement.removeEventListener("drop", handleDrop);
+ }
+ };
+ }, []);
+
+ return (
+ <div
+ className={`h-full w-full absolute top-0 left-0 z-[100000] pointer-events-none ${isDraggingOver && "bg-[#2C3439] pointer-events-auto"}`}
+ ref={dropRef}
+ ></div>
+ );
+}
+
+export default DropZone;
diff --git a/apps/web/app/(canvas)/enabledComp.tsx b/apps/web/app/(canvas)/enabledComp.tsx
index 5dbe6ee7..85811b82 100644
--- a/apps/web/app/(canvas)/enabledComp.tsx
+++ b/apps/web/app/(canvas)/enabledComp.tsx
@@ -7,12 +7,12 @@ export const components: Partial<TLUiComponents> = {
TopPanel: null,
DebugPanel: null,
DebugMenu: null,
+ PageMenu: null,
// Minimap: null,
// ContextMenu: null,
// HelpMenu: null,
// ZoomMenu: null,
// StylePanel: null,
- // PageMenu: null,
// NavigationPanel: null,
// Toolbar: null,
// KeyboardShortcutsDialog: null,
diff --git a/apps/web/app/(canvas)/lib/context.tsx b/apps/web/app/(canvas)/lib/context.tsx
new file mode 100644
index 00000000..36a106cf
--- /dev/null
+++ b/apps/web/app/(canvas)/lib/context.tsx
@@ -0,0 +1,11 @@
+import { createContext } from 'react';
+
+export interface DragContextType {
+ isDraggingOver: boolean;
+ setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+
+const DragContext = createContext<DragContextType | undefined>(undefined);
+
+export default DragContext; \ No newline at end of file
diff --git a/apps/web/app/(canvas)/lib/createEmbeds.ts b/apps/web/app/(canvas)/lib/createEmbeds.ts
index 322e697e..0db3c71b 100644
--- a/apps/web/app/(canvas)/lib/createEmbeds.ts
+++ b/apps/web/app/(canvas)/lib/createEmbeds.ts
@@ -2,8 +2,8 @@ import { AssetRecordType, Editor, TLAsset, TLAssetId, TLBookmarkShape, TLExterna
export default async function createEmbedsFromUrl({url, point, sources, editor}: {
url: string
- point: VecLike | undefined
- sources: TLExternalContentSource[] | undefined
+ point?: VecLike | undefined
+ sources?: TLExternalContentSource[] | undefined
editor: Editor
}){
@@ -50,10 +50,18 @@ export default async function createEmbedsFromUrl({url, point, sources, editor}:
type: "url",
url,
});
- const fetchWebsite = await (await fetch(`https://unfurl-bookmark.pruthvirajthinks.workers.dev/?url=${url}`)).json()
- if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
- if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
- if (fetchWebsite.description) bookmarkAsset.props.description = fetchWebsite.description;
+ const fetchWebsite: {
+ title?: string;
+ image?: string;
+ description?: string;
+ } = await (await fetch(`/api/unfirlsite?website=${url}`, {
+ method: "POST"
+ })).json()
+ if (bookmarkAsset){
+ if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
+ if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
+ if (fetchWebsite.description) bookmarkAsset.props.description = fetchWebsite.description;
+ }
if (!bookmarkAsset) throw Error("Could not create an asset");
asset = bookmarkAsset;
} catch (e) {
@@ -79,6 +87,38 @@ export default async function createEmbedsFromUrl({url, point, sources, editor}:
});
}
+function isURL(str: string) {
+ try {
+ new URL(str);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+
+export function handleExternalDroppedContent({text, editor}: {text:string, editor: Editor}){
+ const position = editor.inputs.shiftKey
+ ? editor.inputs.currentPagePoint
+ : editor.getViewportPageBounds().center;
+
+ if (isURL(text)){
+ createEmbedsFromUrl({editor, url: text})
+ } else{
+ editor.createShape({
+ type: "text",
+ x: position.x - 75,
+ y: position.y - 75,
+ props: {
+ text: text,
+ size: "s",
+ textAlign: "start",
+ },
+ });
+
+ }
+}
+
function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
// Re-position shapes so that the center of the group is at the provided point
const viewportPageBounds = editor.getViewportPageBounds()
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index bb6a0be1..32fd1fce 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -1,7 +1,7 @@
"use client";
import { AnimatePresence } from "framer-motion";
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import QueryInput from "../home/queryinput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
@@ -19,7 +19,10 @@ 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";
+import { z } from "zod";
+import { toast } from "sonner";
+import Link from "next/link";
function ChatWindow({
q,
@@ -33,19 +36,85 @@ function ChatWindow({
{
question: q,
answer: {
- parts: [
- // {
- // text: `It seems like there might be a typo in your question. Could you please clarify or provide more context? If you meant "interesting," please let me know what specific information or topic you find interesting, and I can help you with that.`,
- // },
- ],
+ parts: [],
sources: [],
},
},
]);
+ const [isAutoScroll, setIsAutoScroll] = useState(true);
+
+ const removeJustificationFromText = (text: string) => {
+ // remove everything after the first "<justification>" word
+ const justificationLine = text.indexOf("<justification>");
+ if (justificationLine !== -1) {
+ // Add that justification to the last chat message
+ const lastChatMessage = chatHistory[chatHistory.length - 1];
+ if (lastChatMessage) {
+ lastChatMessage.answer.justification = text.slice(justificationLine);
+ }
+ return text.slice(0, justificationLine);
+ }
+ return text;
+ };
const router = useRouter();
const getAnswer = async (query: string, spaces: string[]) => {
+ const sourcesFetch = await fetch(
+ `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true`,
+ {
+ method: "POST",
+ body: JSON.stringify({ chatHistory }),
+ },
+ );
+
+ // TODO: handle this properly
+ const sources = await sourcesFetch.json();
+
+ const sourcesZod = z.object({
+ ids: z.array(z.string()),
+ metadata: z.array(z.any()),
+ });
+
+ const sourcesParsed = sourcesZod.safeParse(sources);
+
+ if (!sourcesParsed.success) {
+ console.log(sources);
+ console.error(sourcesParsed.error);
+ toast.error("Something went wrong while getting the sources");
+ return;
+ }
+
+ setChatHistory((prevChatHistory) => {
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior: "smooth",
+ });
+ const newChatHistory = [...prevChatHistory];
+ const lastAnswer = newChatHistory[newChatHistory.length - 1];
+ if (!lastAnswer) return prevChatHistory;
+ const filteredSourceUrls = new Set(
+ sourcesParsed.data.metadata.map((source) => source.url),
+ );
+ const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
+ if (filteredSourceUrls.has(source.url)) {
+ filteredSourceUrls.delete(source.url);
+ return true;
+ }
+ return false;
+ });
+ lastAnswer.answer.sources = uniqueSources.map((source) => ({
+ title: source.title ?? "Untitled",
+ type: source.type ?? "page",
+ source: source.url ?? "https://supermemory.ai",
+ content: source.description ?? "No content available",
+ numChunks: sourcesParsed.data.metadata.filter(
+ (f) => f.url === source.url,
+ ).length,
+ }));
+ return newChatHistory;
+ });
+
const resp = await fetch(`/api/chat?q=${query}&spaces=${spaces}`, {
method: "POST",
body: JSON.stringify({ chatHistory }),
@@ -53,7 +122,6 @@ function ChatWindow({
const reader = resp.body?.getReader();
let done = false;
- let result = "";
while (!done && reader) {
const { value, done: d } = await reader.read();
done = d;
@@ -62,23 +130,28 @@ function ChatWindow({
const newChatHistory = [...prevChatHistory];
const lastAnswer = newChatHistory[newChatHistory.length - 1];
if (!lastAnswer) return prevChatHistory;
- lastAnswer.answer.parts.push({ text: new TextDecoder().decode(value) });
+ const txt = new TextDecoder().decode(value);
+
+ if (isAutoScroll) {
+ window.scrollTo({
+ top: document.documentElement.scrollHeight,
+ behavior: "smooth",
+ });
+ }
+
+ lastAnswer.answer.parts.push({ text: txt });
return newChatHistory;
});
}
-
- console.log(result);
};
useEffect(() => {
if (q.trim().length > 0) {
+ setLayout("chat");
getAnswer(
q,
spaces.map((s) => s.id),
);
- setTimeout(() => {
- setLayout("chat");
- }, 300);
} else {
router.push("/home");
}
@@ -94,18 +167,23 @@ function ChatWindow({
className="max-w-3xl h-full justify-center items-center flex mx-auto w-full flex-col"
>
<div className="w-full h-96">
- <QueryInput initialQuery={q} initialSpaces={[]} disabled />
+ <QueryInput
+ handleSubmit={() => {}}
+ initialQuery={q}
+ initialSpaces={[]}
+ disabled
+ />
</div>
</motion.div>
) : (
<div
- className="max-w-3xl flex mx-auto w-full flex-col mt-24"
+ className="max-w-3xl relative flex mx-auto w-full flex-col mt-24 pb-32"
key="chat"
>
{chatHistory.map((chat, idx) => (
<div
key={idx}
- className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b" : ""}`}
+ className={`mt-8 ${idx != chatHistory.length - 1 ? "pb-2 border-b border-b-gray-400" : ""}`}
>
<h2
className={cn(
@@ -151,15 +229,25 @@ function ChatWindow({
</>
))}
{chat.answer.sources.map((source, idx) => (
- <div
+ <Link
+ href={source.source}
key={idx}
className="rounded-xl bg-secondary p-4 flex flex-col gap-2 min-w-72"
>
- <div className="text-foreground-menu">
- {source.type}
+ <div className="flex justify-between text-foreground-menu text-sm">
+ <span>{source.type}</span>
+
+ {source.numChunks > 1 && (
+ <span>{source.numChunks} chunks</span>
+ )}
+ </div>
+ <div className="text-base">{source.title}</div>
+ <div className="text-xs">
+ {source.content.length > 100
+ ? source.content.slice(0, 100) + "..."
+ : source.content}
</div>
- <div>{source.title}</div>
- </div>
+ </Link>
))}
</AccordionContent>
</AccordionItem>
@@ -197,14 +285,67 @@ function ChatWindow({
}}
className="flex flex-col gap-2"
>
- {chat.answer.parts.map((part) => part.text).join("")}
+ {removeJustificationFromText(
+ chat.answer.parts.map((part) => part.text).join(""),
+ )}
</Markdown>
</div>
</div>
-
+ {/* Justification */}
+ {chat.answer.justification &&
+ chat.answer.justification.length && (
+ <div
+ className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
+ >
+ <Accordion defaultValue={""} type="single" collapsible>
+ <AccordionItem value="justification">
+ <AccordionTrigger className="text-foreground-menu">
+ Justification
+ </AccordionTrigger>
+ <AccordionContent
+ className="relative flex gap-2 max-w-3xl overflow-auto no-scrollbar"
+ defaultChecked
+ >
+ {chat.answer.justification.length > 0
+ ? chat.answer.justification
+ .replaceAll("<justification>", "")
+ .replaceAll("</justification>", "")
+ : "No justification provided."}
+ </AccordionContent>
+ </AccordionItem>
+ </Accordion>
+ </div>
+ )}
</div>
</div>
))}
+
+ <div className="fixed bottom-0 w-full max-w-3xl pb-4">
+ <QueryInput
+ mini
+ className="w-full shadow-md"
+ initialQuery={""}
+ initialSpaces={[]}
+ handleSubmit={async (q, spaces) => {
+ setChatHistory((prevChatHistory) => {
+ return [
+ ...prevChatHistory,
+ {
+ question: q,
+ answer: {
+ parts: [],
+ sources: [],
+ },
+ },
+ ];
+ });
+ await getAnswer(
+ q,
+ spaces.map((s) => `${s.id}`),
+ );
+ }}
+ />
+ </div>
</div>
)}
</AnimatePresence>
diff --git a/apps/web/app/(dash)/dynamicisland.tsx b/apps/web/app/(dash)/dynamicisland.tsx
index c08f883a..98fafc7a 100644
--- a/apps/web/app/(dash)/dynamicisland.tsx
+++ b/apps/web/app/(dash)/dynamicisland.tsx
@@ -4,12 +4,12 @@ import { AddIcon } from "@repo/ui/icons";
import Image from "next/image";
import { AnimatePresence, useMotionValueEvent, useScroll } from "framer-motion";
-import { useEffect, useRef, useState } from "react";
+import { useActionState, useEffect, useRef, useState } from "react";
import { motion } from "framer-motion";
import { Label } from "@repo/ui/shadcn/label";
import { Input } from "@repo/ui/shadcn/input";
import { Textarea } from "@repo/ui/shadcn/textarea";
-import { createSpace } from "../actions/doers";
+import { createMemory, createSpace } from "../actions/doers";
import {
Select,
SelectContent,
@@ -20,6 +20,7 @@ import {
import { Space } from "../actions/types";
import { getSpaces } from "../actions/fetchers";
import { toast } from "sonner";
+import { useFormStatus } from "react-dom";
export function DynamicIsland() {
const { scrollYProgress } = useScroll();
@@ -253,13 +254,39 @@ function PageForm({
cancelfn: () => void;
spaces: Space[];
}) {
+ const [loading, setLoading] = useState(false);
+
+ const { pending } = useFormStatus();
return (
- <div className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3">
+ <form
+ action={async (e: FormData) => {
+ const content = e.get("content")?.toString();
+ const space = e.get("space")?.toString();
+ if (!content) {
+ toast.error("Content is required");
+ return;
+ }
+ setLoading(true);
+ const cont = await createMemory({
+ content: content,
+ spaces: space ? [space] : undefined,
+ });
+
+ console.log(cont);
+ setLoading(false);
+ if (cont.success) {
+ toast.success("Memory created");
+ } else {
+ toast.error("Memory creation failed");
+ }
+ }}
+ className="bg-secondary border border-muted-foreground px-4 py-3 rounded-2xl mt-2 flex flex-col gap-3"
+ >
<div>
<Label className="text-[#858B92]" htmlFor="space">
Space
</Label>
- <Select>
+ <Select name="space">
<SelectTrigger>
<SelectValue placeholder="Space" />
</SelectTrigger>
@@ -272,24 +299,28 @@ function PageForm({
</SelectContent>
</Select>
</div>
+ <div key={`${loading}-${pending}`}>
+ {loading ? <div>Loading...</div> : "not loading"}
+ </div>
<div>
<Label className="text-[#858B92]" htmlFor="name">
Page Url
</Label>
<Input
className="bg-[#2B3237] focus-visible:ring-0 border-none focus-visible:ring-offset-0"
- id="name"
+ id="input"
+ name="content"
/>
</div>
<div className="flex justify-end">
- <div
- onClick={cancelfn}
+ <button
+ type="submit"
className="bg-[#2B3237] px-2 py-1 rounded-xl cursor-pointer"
>
- cancel
- </div>
+ Submit
+ </button>
</div>
- </div>
+ </form>
);
}
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index c539673d..bdf6a61e 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -1,11 +1,12 @@
-import React from "react";
-import Menu from "../menu";
-import Header from "../header";
+"use client";
+
+import React, { useEffect, useState } from "react";
import QueryInput from "./queryinput";
-import { homeSearchParamsCache } from "@/app/helpers/lib/searchParams";
+import { homeSearchParamsCache } from "@/lib/searchParams";
import { getSpaces } from "@/app/actions/fetchers";
+import { useRouter } from "next/navigation";
-async function Page({
+function Page({
searchParams,
}: {
searchParams: Record<string, string | string[] | undefined>;
@@ -13,12 +14,18 @@ async function Page({
// TODO: use this to show a welcome page/modal
const { firstTime } = homeSearchParamsCache.parse(searchParams);
- let spaces = await getSpaces();
+ const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
+
+ useEffect(() => {
+ getSpaces().then((res) => {
+ if (res.success && res.data) {
+ setSpaces(res.data);
+ }
+ // TODO: HANDLE ERROR
+ });
+ }, []);
- if (!spaces.success) {
- // TODO: handle this error properly.
- spaces.data = [];
- }
+ const { push } = useRouter();
return (
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col">
@@ -26,7 +33,17 @@ async function Page({
{/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
<div className="w-full pb-20">
- <QueryInput initialSpaces={spaces.data} />
+ <QueryInput
+ handleSubmit={(q, spaces) => {
+ const newQ =
+ "/chat?q=" +
+ encodeURI(q) +
+ (spaces ? "&spaces=" + JSON.stringify(spaces) : "");
+
+ push(newQ);
+ }}
+ initialSpaces={spaces}
+ />
</div>
</div>
);
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index fbd537e3..4fadfb6f 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -12,6 +12,9 @@ function QueryInput({
initialQuery = "",
initialSpaces = [],
disabled = false,
+ className,
+ mini = false,
+ handleSubmit,
}: {
initialQuery?: string;
initialSpaces?: {
@@ -19,32 +22,14 @@ function QueryInput({
name: string;
}[];
disabled?: boolean;
+ className?: string;
+ mini?: boolean;
+ handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
}) {
const [q, setQ] = useState(initialQuery);
const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
- const { push } = useRouter();
-
- const parseQ = () => {
- // preparedSpaces is list of spaces selected by user, with id and name
- const preparedSpaces = initialSpaces
- .filter((x) => selectedSpaces.includes(x.id))
- .map((x) => {
- return {
- id: x.id,
- name: x.name,
- };
- });
-
- const newQ =
- "/chat?q=" +
- encodeURI(q) +
- (selectedSpaces ? "&spaces=" + JSON.stringify(preparedSpaces) : "");
-
- return newQ;
- };
-
const options = useMemo(
() =>
initialSpaces.map((x) => ({
@@ -54,21 +39,43 @@ function QueryInput({
[initialSpaces],
);
+ const preparedSpaces = useMemo(
+ () =>
+ initialSpaces
+ .filter((x) => selectedSpaces.includes(x.id))
+ .map((x) => {
+ return {
+ id: x.id,
+ name: x.name,
+ };
+ }),
+ [selectedSpaces, initialSpaces],
+ );
+
return (
- <div>
- <div className="bg-secondary rounded-t-[24px]">
+ <div className={className}>
+ <div
+ className={`bg-secondary ${!mini ? "rounded-t-3xl" : "rounded-3xl"}`}
+ >
{/* input and action button */}
- <form action={async () => push(parseQ())} className="flex gap-4 p-3">
+ <form
+ action={async () => {
+ handleSubmit(q, preparedSpaces);
+ setQ("");
+ }}
+ className="flex gap-4 p-3"
+ >
<textarea
name="q"
cols={30}
- rows={4}
+ rows={mini ? 2 : 4}
className="bg-transparent pt-2.5 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full p-4"
placeholder="Ask your second brain..."
onKeyDown={(e) => {
if (e.key === "Enter") {
e.preventDefault();
- if (!e.shiftKey) push(parseQ());
+ handleSubmit(q, preparedSpaces);
+ setQ("");
}
}}
onChange={(e) => setQ(e.target.value)}
@@ -85,24 +92,29 @@ function QueryInput({
<Image src={ArrowRightIcon} alt="Right arrow icon" />
</button>
</form>
-
- <Divider />
</div>
{/* selected sources */}
- <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-[24px]">
- <MultipleSelector
- key={options.length}
- disabled={disabled}
- defaultOptions={options}
- onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))}
- placeholder="Focus on specific spaces..."
- emptyIndicator={
- <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
- no results found.
- </p>
- }
- />
- </div>
+ {!mini && (
+ <>
+ <Divider />
+ <div className="flex items-center gap-6 p-2 h-auto bg-secondary rounded-b-3xl">
+ <MultipleSelector
+ key={options.length}
+ disabled={disabled}
+ defaultOptions={options}
+ onChange={(e) =>
+ setSelectedSpaces(e.map((x) => parseInt(x.value)))
+ }
+ placeholder="Focus on specific spaces..."
+ emptyIndicator={
+ <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
+ no results found.
+ </p>
+ }
+ />
+ </div>
+ </>
+ )}
</div>
);
}
diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx
index 4c787c9c..4e1f6989 100644
--- a/apps/web/app/(dash)/layout.tsx
+++ b/apps/web/app/(dash)/layout.tsx
@@ -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 }) {
diff --git a/apps/web/app/(dash)/memories/page.tsx b/apps/web/app/(dash)/memories/page.tsx
index bc2fcd53..ff746d1d 100644
--- a/apps/web/app/(dash)/memories/page.tsx
+++ b/apps/web/app/(dash)/memories/page.tsx
@@ -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;
diff --git a/apps/web/app/(editor)/components/aigenerate.tsx b/apps/web/app/(editor)/components/aigenerate.tsx
index b1c4ccd4..de9b2a3f 100644
--- a/apps/web/app/(editor)/components/aigenerate.tsx
+++ b/apps/web/app/(editor)/components/aigenerate.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useRef, useState } from "react";
+import React, { useEffect, useRef, useState } from "react";
import Magic from "./ui/magic";
import CrazySpinner from "./ui/crazy-spinner";
import Asksvg from "./ui/asksvg";
@@ -8,14 +8,11 @@ import Autocompletesvg from "./ui/autocompletesvg";
import { motion, AnimatePresence } from "framer-motion";
import type { Editor } from "@tiptap/core";
import { useEditor } from "novel";
-
+import { NodeSelection } from "prosemirror-state";
function Aigenerate() {
const [visible, setVisible] = useState(false);
const [generating, setGenerating] = useState(false);
-
- // generating -> can be converted to false, so we need to make sure the generation gets cancelled
- // visible
const { editor } = useEditor();
const setGeneratingfn = (v: boolean) => setGenerating(v);
@@ -58,8 +55,9 @@ function Aigenerate() {
}}
className="absolute z-50 top-0"
>
- <ToolBar setGeneratingfn={setGeneratingfn} editor={editor} />
- <div className="h-8 w-18rem bg-blue-600 blur-[16rem]" />
+ {/* TODO: handle Editor not initalised, maybe with a loading state. */}
+ <ToolBar setGeneratingfn={setGeneratingfn} editor={editor} />
+ <div className="h-8 w-18rem bg-blue-600 blur-[16rem]" />
</motion.div>
</div>
);
@@ -68,10 +66,22 @@ function Aigenerate() {
export default Aigenerate;
const options = [
- <><Translatesvg />Translate</>,
- <><Rewritesvg />Change Tone</>,
- <><Asksvg />Ask Gemini</>,
- <><Autocompletesvg />Auto Complete</>
+ <>
+ <Translatesvg />
+ Translate
+ </>,
+ <>
+ <Rewritesvg />
+ Change Tone
+ </>,
+ <>
+ <Asksvg />
+ Ask Gemini
+ </>,
+ <>
+ <Autocompletesvg />
+ Auto Complete
+ </>,
];
function ToolBar({
@@ -116,7 +126,7 @@ function ToolBar({
)}
</AnimatePresence>
<div className="select-none flex items-center whitespace-nowrap gap-3 relative z-[60] pointer-events-none">
- {item}
+ {item}
</div>
</div>
))}
@@ -135,43 +145,42 @@ async function AigenerateContent({
}) {
setGeneratingfn(true);
- const {from, to} = editor.view.state.selection;
- const content = editor.view.state.selection.content();
- content.content.forEach((v, i)=> {
- v.forEach((v, i)=> {
- console.log(v.text)
- })
- })
-
- const transaction = editor.state.tr
- transaction.replaceRange(from, to, content)
-
- editor.view.dispatch(transaction)
-
- // console.log(content)
- // content.map((v, i)=> console.log(v.content))
-
- // const fragment = Fragment.fromArray(content);
-
- // console.log(fragment)
-
- // editor.view.state.selection.content().content.append(content)
+ const { from, to } = editor.view.state.selection;
+
+ const slice = editor.state.selection.content();
+ const text = editor.storage.markdown.serializer.serialize(slice.content);
+
+ const request = [
+ "Translate to hindi written in english, do not write anything else",
+ "change tone, improve the way be more formal",
+ "ask, answer the question",
+ "continue this, minimum 80 characters, do not repeat just continue don't use ... to denote start",
+ ];
+
+ const resp = await fetch("/api/editorai", {
+ method: "POST",
+ body: JSON.stringify({
+ context: text,
+ request: request[idx],
+ }),
+ });
+
+ const reader = resp.body?.getReader();
+ let done = false;
+ let position = to;
+ while (!done && reader) {
+ const { value, done: d } = await reader.read();
+ done = d;
+
+ const decoded = new TextDecoder().decode(value);
+ console.log(decoded);
+ editor
+ .chain()
+ .focus()
+ .insertContentAt(position + 1, decoded)
+ .run();
+ position += decoded.length;
+ }
setGeneratingfn(false);
-
-
-
- // const genAI = new GoogleGenerativeAI("AIzaSyDGwJCP9SH5gryyvh65LJ6xTZ0SOdNvzyY");
- // const model = genAI.getGenerativeModel({ model: "gemini-pro"});
-
- // const result = (await model.generateContent(`${ty}, ${query}`)).response.text();
-
- // .insertContentAt(
- // {
- // from: from,
- // to: to,
- // },
- // result,
- // )
- // .run();
}
diff --git a/apps/web/app/(editor)/editor.tsx b/apps/web/app/(editor)/editor.tsx
index 5b4a60ce..f7f9a098 100644
--- a/apps/web/app/(editor)/editor.tsx
+++ b/apps/web/app/(editor)/editor.tsx
@@ -15,19 +15,20 @@ import Topbar from "./components/topbar";
const Editor = () => {
const [initialContent, setInitialContent] = useState<null | JSONContent>(
- null
+ null,
);
const [saveStatus, setSaveStatus] = useState("Saved");
const [charsCount, setCharsCount] = useState();
const [visible, setVisible] = useState(true);
useEffect(() => {
+ if (typeof window === "undefined") return;
const content = window.localStorage.getItem("novel-content");
if (content) setInitialContent(JSON.parse(content));
else setInitialContent(defaultEditorContent);
}, []);
- if (!initialContent) return null;
+ if (!initialContent) return <>Loading...</>;
return (
<div className="relative w-full max-w-screen-xl">
diff --git a/apps/web/app/(landing)/Cta.tsx b/apps/web/app/(landing)/Cta.tsx
index be99bf99..f0f471c2 100644
--- a/apps/web/app/(landing)/Cta.tsx
+++ b/apps/web/app/(landing)/Cta.tsx
@@ -24,7 +24,7 @@ function Cta() {
height={1405}
priority
draggable="false"
- className="absolute z-[-2] hidden select-none rounded-3xl bg-black md:block lg:w-[80%]"
+ className="absolute z-[-2] hidden select-none rounded-3xl bg-background md:block lg:w-[80%]"
/>
<h1 className="z-20 mt-4 text-center text-5xl font-medium tracking-tight text-white">
Your bookmarks are collecting dust.
diff --git a/apps/web/app/(landing)/page.tsx b/apps/web/app/(landing)/page.tsx
index 09f94d92..5f8b28b4 100644
--- a/apps/web/app/(landing)/page.tsx
+++ b/apps/web/app/(landing)/page.tsx
@@ -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";
diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts
index c8a1f3b4..6c7180d9 100644
--- a/apps/web/app/actions/doers.ts
+++ b/apps/web/app/actions/doers.ts
@@ -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,223 @@ 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(input.content);
+ } 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 = [];
+ }
+
+ 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,
+ spaces: storeToSpaces,
+ user: data.user.id,
+ type,
+ }),
+ headers: {
+ "Content-Type": "application/json",
+ Authorization: "Bearer " + process.env.BACKEND_SECURITY_KEY,
+ },
+ },
+ );
+
+ if (!vectorSaveResponse.ok) {
+ const errorData = await vectorSaveResponse.text();
+ console.log(errorData);
+ return {
+ success: false,
+ data: 0,
+ error: `Failed to save to vector store. Backend returned error: ${errorData}`,
+ };
+ }
+
+ // 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.id,
+ storeToSpaces.map((s) => parseInt(s)),
+ ),
+ eq(space.user, data.user.id),
+ ),
+ )
+ .all();
+
+ await Promise.all(
+ spaceData.map(async (space) => {
+ await db
+ .insert(contentToSpace)
+ .values({ contentId: contentId, spaceId: space.id });
+ }),
+ );
+ }
+
+ 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}`,
+ };
+ }
+};
diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts
index 9c2527f0..dc71252e 100644
--- a/apps/web/app/actions/fetchers.ts
+++ b/apps/web/app/actions/fetchers.ts
@@ -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 },
+ };
+};
diff --git a/apps/web/app/actions/types.ts b/apps/web/app/actions/types.ts
index fbf669e2..5c5afc5c 100644
--- a/apps/web/app/actions/types.ts
+++ b/apps/web/app/actions/types.ts
@@ -1,6 +1,7 @@
export type Space = {
id: number;
name: string;
+ numberOfMemories?: number;
};
export type ServerActionReturnType<T> = Promise<{
diff --git a/apps/web/app/api/[...nextauth]/route.ts b/apps/web/app/api/[...nextauth]/route.ts
index 50807ab1..e19cc16e 100644
--- a/apps/web/app/api/[...nextauth]/route.ts
+++ b/apps/web/app/api/[...nextauth]/route.ts
@@ -1,2 +1,2 @@
-export { GET, POST } from "../../helpers/server/auth";
+export { GET, POST } from "../../../server/auth";
export const runtime = "edge";
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index aba8784c..c19ce92b 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -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.id}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_SECURITY_KEY}`,
diff --git a/apps/web/app/api/editorai/route.ts b/apps/web/app/api/editorai/route.ts
new file mode 100644
index 00000000..5e1fbf0c
--- /dev/null
+++ b/apps/web/app/api/editorai/route.ts
@@ -0,0 +1,30 @@
+import type { NextRequest } from "next/server";
+import { ensureAuth } from "../ensureAuth";
+
+export const runtime = "edge";
+
+// ERROR #2 - This the the next function that calls the backend, I sometimes think this is redundency, but whatever
+// I have commented the auth code, It should not work in development, but it still does sometimes
+export async function POST(request: NextRequest) {
+ // const d = await ensureAuth(request);
+ // if (!d) {
+ // return new Response("Unauthorized", { status: 401 });
+ // }
+ const res : {context: string, request: string} = await request.json()
+
+ try {
+ const resp = await fetch(`${process.env.BACKEND_BASE_URL}/api/editorai?context=${res.context}&request=${res.request}`);
+ // this just checks if there are erros I am keeping it commented for you to better understand the important pieces
+ // if (resp.status !== 200 || !resp.ok) {
+ // const errorData = await resp.text();
+ // console.log(errorData);
+ // return new Response(
+ // JSON.stringify({ message: "Error in CF function", error: errorData }),
+ // { status: resp.status },
+ // );
+ // }
+ return new Response(resp.body, { status: 200 });
+ } catch (error) {
+ return new Response(`Error, ${error}`)
+ }
+} \ No newline at end of file
diff --git a/apps/web/app/api/ensureAuth.ts b/apps/web/app/api/ensureAuth.ts
index a1401a07..d2fbac0b 100644
--- a/apps/web/app/api/ensureAuth.ts
+++ b/apps/web/app/api/ensureAuth.ts
@@ -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) {
diff --git a/apps/web/app/api/getCount/route.ts b/apps/web/app/api/getCount/route.ts
index f760c145..7cd2a2d3 100644
--- a/apps/web/app/api/getCount/route.ts
+++ b/apps/web/app/api/getCount/route.ts
@@ -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"),
),
);
diff --git a/apps/web/app/api/me/route.ts b/apps/web/app/api/me/route.ts
index 20b6aece..621dcbfe 100644
--- a/apps/web/app/api/me/route.ts
+++ b/apps/web/app/api/me/route.ts
@@ -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";
diff --git a/apps/web/app/api/spaces/route.ts b/apps/web/app/api/spaces/route.ts
index c46b02fc..cbed547d 100644
--- a/apps/web/app/api/spaces/route.ts
+++ b/apps/web/app/api/spaces/route.ts
@@ -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";
diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts
index f96f90cf..cb10db24 100644
--- a/apps/web/app/api/store/route.ts
+++ b/apps/web/app/api/store/route.ts
@@ -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 });
diff --git a/apps/web/app/api/unfirlsite/route.ts b/apps/web/app/api/unfirlsite/route.ts
new file mode 100644
index 00000000..4b8b4858
--- /dev/null
+++ b/apps/web/app/api/unfirlsite/route.ts
@@ -0,0 +1,134 @@
+import { load } from 'cheerio'
+import { AwsClient } from "aws4fetch";
+
+import type { NextRequest } from "next/server";
+import { ensureAuth } from "../ensureAuth";
+
+export const runtime = "edge";
+
+const r2 = new AwsClient({
+ accessKeyId: process.env.R2_ACCESS_KEY_ID,
+ secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
+});
+
+
+export async function POST(request: NextRequest) {
+
+ const d = await ensureAuth(request);
+ if (!d) {
+ return new Response("Unauthorized", { status: 401 });
+ }
+
+ if (
+ !process.env.R2_ACCESS_KEY_ID ||
+ !process.env.R2_ACCOUNT_ID ||
+ !process.env.R2_SECRET_ACCESS_KEY ||
+ !process.env.R2_BUCKET_NAME
+ ) {
+ return new Response(
+ "Missing one or more R2 env variables: R2_ENDPOINT, R2_ACCESS_ID, R2_SECRET_KEY, R2_BUCKET_NAME. To get them, go to the R2 console, create and paste keys in a `.dev.vars` file in the root of this project.",
+ { status: 500 },
+ );
+ }
+
+ const website = new URL(request.url).searchParams.get("website");
+
+ if (!website) {
+ return new Response("Missing website", { status: 400 });
+ }
+
+ const salt = () => Math.floor(Math.random() * 11);
+ const encodeWebsite = `${encodeURIComponent(website)}${salt()}`;
+
+ try {
+ // this returns the og image, description and title of website
+ const response = await unfurl(website);
+
+ if (!response.image){
+ return new Response(JSON.stringify(response))
+ }
+
+ const imageUrl = await process.env.DEV_IMAGES.get(encodeWebsite)
+ if (imageUrl){
+ return new Response(JSON.stringify({
+ image: imageUrl,
+ title: response.title,
+ description: response.description,
+ }))
+ }
+
+ const res = await fetch(`${response.image}`)
+ const image = await res.blob();
+
+ const url = new URL(
+ `https://${process.env.R2_BUCKET_NAME}.${process.env.R2_ACCOUNT_ID}.r2.cloudflarestorage.com`
+ );
+
+ url.pathname = encodeWebsite;
+ url.searchParams.set("X-Amz-Expires", "3600");
+
+ const signedPuturl = await r2.sign(
+ new Request(url, {
+ method: "PUT",
+ }),
+ {
+ aws: { signQuery: true },
+ }
+ );
+ await fetch(signedPuturl.url, {
+ method: 'PUT',
+ body: image,
+ });
+
+ await process.env.DEV_IMAGES.put(encodeWebsite, `${process.env.R2_PUBLIC_BUCKET_ADDRESS}/${encodeWebsite}`)
+
+ return new Response(JSON.stringify({
+ image: `${process.env.R2_PUBLIC_BUCKET_ADDRESS}/${encodeWebsite}`,
+ title: response.title,
+ description: response.description,
+ }));
+
+ } catch (error) {
+ console.log(error)
+ return new Response(JSON.stringify({
+ status: 500,
+ error: error,
+ }))
+ }
+ }
+
+export async function unfurl(url: string) {
+ const response = await fetch(url)
+ if (response.status >= 400) {
+ throw new Error(`Error fetching url: ${response.status}`)
+ }
+ const contentType = response.headers.get('content-type')
+ if (!contentType?.includes('text/html')) {
+ throw new Error(`Content-type not right: ${contentType}`)
+ }
+
+ const content = await response.text()
+ const $ = load(content)
+
+ const og: { [key: string]: string | undefined } = {}
+ const twitter: { [key: string]: string | undefined } = {}
+
+ // @ts-ignore, it just works so why care of type safety if someone has better way go ahead
+ $('meta[property^=og:]').each((_, el) => (og[$(el).attr('property')!] = $(el).attr('content')))
+ // @ts-ignore
+ $('meta[name^=twitter:]').each((_, el) => (twitter[$(el).attr('name')!] = $(el).attr('content')))
+
+ const title = og['og:title'] ?? twitter['twitter:title'] ?? $('title').text() ?? undefined
+ const description =
+ og['og:description'] ??
+ twitter['twitter:description'] ??
+ $('meta[name="description"]').attr('content') ??
+ undefined
+ const image = og['og:image:secure_url'] ?? og['og:image'] ?? twitter['twitter:image'] ?? undefined
+
+ return {
+ title,
+ description,
+ image,
+ }
+}
diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx
index 9ace733a..b51a16bb 100644
--- a/apps/web/app/ref/page.tsx
+++ b/apps/web/app/ref/page.tsx
@@ -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";
diff --git a/apps/web/cf-env.d.ts b/apps/web/cf-env.d.ts
index 98303f35..be5c991a 100644
--- a/apps/web/cf-env.d.ts
+++ b/apps/web/cf-env.d.ts
@@ -1,6 +1,17 @@
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_KEY_ID: string;
+ R2_SECRET_ACCESS_KEY: string;
+ R2_PUBLIC_BUCKET_ADDRESS: string;
+ R2_BUCKET_NAME: string;
+ BACKEND_SECURITY_KEY: string;
+ BACKEND_BASE_URL: string;
+ }
}
}
diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts
index 2755280c..4f11ba55 100644
--- a/apps/web/env.d.ts
+++ b/apps/web/env.d.ts
@@ -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;
}
diff --git a/apps/web/app/helpers/constants.ts b/apps/web/lib/constants.ts
index c3fc640a..7a9485cf 100644
--- a/apps/web/app/helpers/constants.ts
+++ b/apps/web/lib/constants.ts
@@ -1,3 +1,9 @@
+export const LIMITS = {
+ page: 100,
+ tweet: 1000,
+ note: 1000,
+};
+
export const codeLanguageSubset = [
"python",
"javascript",
diff --git a/apps/web/app/helpers/lib/get-metadata.ts b/apps/web/lib/get-metadata.ts
index 4609e49b..4609e49b 100644
--- a/apps/web/app/helpers/lib/get-metadata.ts
+++ b/apps/web/lib/get-metadata.ts
diff --git a/apps/web/app/helpers/lib/get-theme-button.tsx b/apps/web/lib/get-theme-button.tsx
index 020cc976..020cc976 100644
--- a/apps/web/app/helpers/lib/get-theme-button.tsx
+++ b/apps/web/lib/get-theme-button.tsx
diff --git a/apps/web/app/helpers/lib/handle-errors.ts b/apps/web/lib/handle-errors.ts
index 42cae589..42cae589 100644
--- a/apps/web/app/helpers/lib/handle-errors.ts
+++ b/apps/web/lib/handle-errors.ts
diff --git a/apps/web/app/helpers/lib/searchParams.ts b/apps/web/lib/searchParams.ts
index 9899eaf7..9899eaf7 100644
--- a/apps/web/app/helpers/lib/searchParams.ts
+++ b/apps/web/lib/searchParams.ts
diff --git a/apps/web/app/helpers/server/auth.ts b/apps/web/server/auth.ts
index c4e426d4..c4e426d4 100644
--- a/apps/web/app/helpers/server/auth.ts
+++ b/apps/web/server/auth.ts
diff --git a/apps/web/app/helpers/server/db/index.ts b/apps/web/server/db/index.ts
index 4d671bea..4d671bea 100644
--- a/apps/web/app/helpers/server/db/index.ts
+++ b/apps/web/server/db/index.ts
diff --git a/apps/web/app/helpers/server/db/schema.ts b/apps/web/server/db/schema.ts
index e3e789c6..1ff23c82 100644
--- a/apps/web/app/helpers/server/db/schema.ts
+++ b/apps/web/server/db/schema.ts
@@ -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",
{