aboutsummaryrefslogtreecommitdiff
path: root/apps/web
diff options
context:
space:
mode:
authorDhravya Shah <[email protected]>2024-07-25 17:35:15 -0500
committerDhravya Shah <[email protected]>2024-07-25 17:35:15 -0500
commitc57719446ae95c2bbd432d7b2b6648a23b35c351 (patch)
treec6f7aca777c7f4748cc6dc335fe56fba8725af02 /apps/web
parentadd try catch in api/add for better error handling (diff)
parentughh, regenerated migrations. my bad. (diff)
downloadsupermemory-kush/experimental-thread.tar.xz
supermemory-kush/experimental-thread.zip
solve merge conflictskush/experimental-thread
Diffstat (limited to 'apps/web')
-rw-r--r--apps/web/app/(auth)/onboarding/page.tsx35
-rw-r--r--apps/web/app/(auth)/signin/page.tsx4
-rw-r--r--apps/web/app/(dash)/chat/[chatid]/loading.tsx10
-rw-r--r--apps/web/app/(dash)/chat/[chatid]/page.tsx4
-rw-r--r--apps/web/app/(dash)/chat/chatWindow.tsx48
-rw-r--r--apps/web/app/(dash)/chat/route.ts5
-rw-r--r--apps/web/app/(dash)/dialogContentContainer.tsx2
-rw-r--r--apps/web/app/(dash)/header/autoBreadCrumbs.tsx4
-rw-r--r--apps/web/app/(dash)/header/header.tsx34
-rw-r--r--apps/web/app/(dash)/header/signOutButton.tsx22
-rw-r--r--apps/web/app/(dash)/home/history.tsx106
-rw-r--r--apps/web/app/(dash)/home/page.tsx61
-rw-r--r--apps/web/app/(dash)/home/queryinput.tsx60
-rw-r--r--apps/web/app/(dash)/layout.tsx8
-rw-r--r--apps/web/app/(dash)/menu.tsx521
-rw-r--r--apps/web/app/(landing)/Hero.tsx26
-rw-r--r--apps/web/app/(quicklinks)/ph/route.ts5
-rw-r--r--apps/web/app/actions/doers.ts148
-rw-r--r--apps/web/app/actions/fetchers.ts24
-rw-r--r--apps/web/app/api/chat/route.ts50
-rw-r--r--apps/web/app/api/store/route.ts25
-rw-r--r--apps/web/app/layout.tsx11
-rw-r--r--apps/web/cf-env.d.ts4
-rw-r--r--apps/web/env.d.ts2
-rw-r--r--apps/web/lib/searchParams.ts1
-rw-r--r--apps/web/lib/utils.ts23
-rw-r--r--apps/web/migrations/0000_steep_moira_mactaggert.sql (renamed from apps/web/migrations/0000_exotic_sway.sql)4
-rw-r--r--apps/web/migrations/meta/0000_snapshot.json18
-rw-r--r--apps/web/migrations/meta/_journal.json4
-rw-r--r--apps/web/server/db/index.ts5
-rw-r--r--apps/web/server/db/schema.ts5
-rw-r--r--apps/web/wrangler.toml25
32 files changed, 975 insertions, 329 deletions
diff --git a/apps/web/app/(auth)/onboarding/page.tsx b/apps/web/app/(auth)/onboarding/page.tsx
index 1912a8e8..10dd2325 100644
--- a/apps/web/app/(auth)/onboarding/page.tsx
+++ b/apps/web/app/(auth)/onboarding/page.tsx
@@ -1,17 +1,16 @@
"use client";
-import Link from "next/link";
import {
+ ArrowUturnDownIcon,
ChevronLeftIcon,
ChevronRightIcon,
QuestionMarkCircleIcon,
- ArrowTurnDownLeftIcon,
} from "@heroicons/react/24/solid";
import { CheckIcon, PlusCircleIcon } from "@heroicons/react/24/outline";
import { motion } from "framer-motion";
-import { useState } from "react";
+import { useEffect, useState } from "react";
import { toast } from "sonner";
-import { createMemory } from "@repo/web/app/actions/doers";
+import { completeOnboarding, createMemory } from "@repo/web/app/actions/doers";
import { useRouter } from "next/navigation";
import Logo from "../../../public/logo.svg";
import Image from "next/image";
@@ -20,6 +19,18 @@ import gradientStyle from "../signin/_components/TextGradient/gradient.module.cs
export default function Home() {
const [currStep, setCurrStep] = useState(0);
+ const { push } = useRouter();
+
+ useEffect(() => {
+ const updateDb = async () => {
+ await completeOnboarding();
+ };
+ if (currStep > 3) {
+ updateDb().then(() => {
+ push("/home?q=what%20is%20supermemory");
+ });
+ }
+ }, [currStep]);
return (
<main className="min-h-screen text-sm text-[#B8C4C6] font-geistSans">
@@ -175,7 +186,7 @@ function StepIndicator({
/>
<p>Step: {currStep}/3</p>
<ChevronRightIcon
- className="h-6"
+ className="h-6 cursor-pointer"
onClick={() => currStep <= 3 && setCurrStep(currStep + 1)}
/>
</div>
@@ -300,7 +311,7 @@ function StepThree({ currStep }: { currStep: number }) {
type="submit"
className="rounded-lg bg-[#369DFD1A] p-3 absolute bottom-4 right-2"
>
- <ArrowTurnDownLeftIcon className="w-4 h-4 text-[#369DFD]" />
+ <ArrowUturnDownIcon className="w-4 h-4 text-[#369DFD]" />
</button>
</form>
</li>
@@ -374,6 +385,12 @@ function StepTwo({ currStep }: { currStep: number }) {
}
function Navbar() {
+ const router = useRouter();
+ const handleSkip = async () => {
+ await completeOnboarding();
+ router.push("/home?q=what%20is%20supermemory");
+ };
+
return (
<div className="flex items-center justify-between p-4 fixed top-0 left-0 w-full">
<Image
@@ -382,9 +399,9 @@ function Navbar() {
className="hover:brightness-125 duration-200 size-12"
/>
- <Link href="/home">
- <button className="text-sm">Skip</button>
- </Link>
+ <button className="text-sm" onClick={handleSkip}>
+ Skip
+ </button>
</div>
);
}
diff --git a/apps/web/app/(auth)/signin/page.tsx b/apps/web/app/(auth)/signin/page.tsx
index a31343cd..3b563b90 100644
--- a/apps/web/app/(auth)/signin/page.tsx
+++ b/apps/web/app/(auth)/signin/page.tsx
@@ -18,7 +18,7 @@ async function Signin({
const user = await auth();
if (user) {
- await redirect("/home");
+ redirect("/home");
}
return (
@@ -64,7 +64,7 @@ async function Signin({
action={async () => {
"use server";
await signIn("google", {
- redirectTo: "/home?firstTime=true",
+ redirectTo: "/home",
});
}}
>
diff --git a/apps/web/app/(dash)/chat/[chatid]/loading.tsx b/apps/web/app/(dash)/chat/[chatid]/loading.tsx
index d28961a6..422adb8e 100644
--- a/apps/web/app/(dash)/chat/[chatid]/loading.tsx
+++ b/apps/web/app/(dash)/chat/[chatid]/loading.tsx
@@ -7,9 +7,15 @@ async function Page({
}: {
searchParams: Record<string, string | string[] | undefined>;
}) {
- const q = (searchParams?.q as string) ?? "from_loading";
+ const q = (searchParams?.q as string) ?? "";
return (
- <ChatWindow q={q} spaces={[]} initialChat={undefined} threadId={"idk"} />
+ <ChatWindow
+ proMode={false}
+ q={q}
+ spaces={[]}
+ initialChat={undefined}
+ threadId={"idk"}
+ />
);
}
diff --git a/apps/web/app/(dash)/chat/[chatid]/page.tsx b/apps/web/app/(dash)/chat/[chatid]/page.tsx
index 87fd0b19..29ffb3d8 100644
--- a/apps/web/app/(dash)/chat/[chatid]/page.tsx
+++ b/apps/web/app/(dash)/chat/[chatid]/page.tsx
@@ -9,7 +9,8 @@ async function Page({
params: { chatid: string };
searchParams: Record<string, string | string[] | undefined>;
}) {
- const { firstTime, q, spaces } = chatSearchParamsCache.parse(searchParams);
+ const { firstTime, q, spaces, proMode } =
+ chatSearchParamsCache.parse(searchParams);
let chat: Awaited<ReturnType<typeof getFullChatThread>>;
@@ -31,6 +32,7 @@ async function Page({
spaces={spaces ?? []}
initialChat={chat.data.length > 0 ? chat.data : undefined}
threadId={params.chatid}
+ proMode={proMode}
/>
);
}
diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx
index 28b99c9d..ed65bf7a 100644
--- a/apps/web/app/(dash)/chat/chatWindow.tsx
+++ b/apps/web/app/(dash)/chat/chatWindow.tsx
@@ -35,14 +35,19 @@ function ChatWindow({
parts: [],
sources: [],
},
+ proModeProcessing: {
+ queries: [],
+ },
},
],
threadId,
+ proMode,
}: {
q: string;
spaces: { id: number; name: string }[];
initialChat?: ChatHistory[];
threadId: string;
+ proMode: boolean;
}) {
const [layout, setLayout] = useState<"chat" | "initial">("chat");
const [chatHistory, setChatHistory] = useState<ChatHistory[]>(initialChat);
@@ -63,13 +68,17 @@ function ChatWindow({
const router = useRouter();
- const getAnswer = async (query: string, spaces: string[]) => {
+ const getAnswer = async (
+ query: string,
+ spaces: string[],
+ proMode: boolean = false,
+ ) => {
if (query.trim() === "from_loading" || query.trim().length === 0) {
return;
}
const sourcesFetch = await fetch(
- `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}`,
+ `/api/chat?q=${query}&spaces=${spaces}&sourcesOnly=true&threadId=${threadId}&proMode=${proMode}`,
{
method: "POST",
body: JSON.stringify({ chatHistory }),
@@ -91,6 +100,8 @@ function ChatWindow({
behavior: "smooth",
});
+ let proModeListedQueries: string[] = [];
+
const updateChatHistoryAndFetch = async () => {
// Step 1: Update chat history with the assistant's response
await new Promise((resolve) => {
@@ -123,6 +134,11 @@ function ChatWindow({
).length,
}));
+ lastAnswer.proModeProcessing.queries =
+ sourcesParsed.data.proModeListedQueries ?? [];
+
+ proModeListedQueries = lastAnswer.proModeProcessing.queries;
+
resolve(newChatHistory);
return newChatHistory;
});
@@ -130,7 +146,7 @@ function ChatWindow({
// Step 2: Fetch data from the API
const resp = await fetch(
- `/api/chat?q=${query}&spaces=${spaces}&threadId=${threadId}`,
+ `/api/chat?q=${(query += proModeListedQueries.join(" "))}&spaces=${spaces}&threadId=${threadId}`,
{
method: "POST",
body: JSON.stringify({ chatHistory, sources: sourcesParsed.data }),
@@ -181,6 +197,7 @@ function ChatWindow({
getAnswer(
q,
spaces.map((s) => `${s.id}`),
+ proMode,
);
}
} else {
@@ -224,6 +241,28 @@ function ChatWindow({
{chat.question}
</h2>
+ {chat.proModeProcessing?.queries?.length > 0 && (
+ <div className="flex flex-col mt-2">
+ <div className="text-foreground-menu py-2">
+ Pro Mode
+ </div>
+ <div className="text-base">
+ <div className="flex gap-2 text-base">
+ {chat.proModeProcessing.queries.map(
+ (query, idx) => (
+ <div
+ className="bg-secondary rounded-md p-2"
+ key={`promode-query-${idx}`}
+ >
+ {query}
+ </div>
+ ),
+ )}
+ </div>
+ </div>
+ </div>
+ )}
+
<div className="flex flex-col mt-2">
<div>
<div className="text-foreground-menu py-2">Answer</div>
@@ -407,6 +446,9 @@ function ChatWindow({
parts: [],
sources: [],
},
+ proModeProcessing: {
+ queries: [],
+ },
},
];
});
diff --git a/apps/web/app/(dash)/chat/route.ts b/apps/web/app/(dash)/chat/route.ts
new file mode 100644
index 00000000..94f250ff
--- /dev/null
+++ b/apps/web/app/(dash)/chat/route.ts
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export async function GET() {
+ return redirect("/home");
+}
diff --git a/apps/web/app/(dash)/dialogContentContainer.tsx b/apps/web/app/(dash)/dialogContentContainer.tsx
index aae71237..4e8d81ef 100644
--- a/apps/web/app/(dash)/dialogContentContainer.tsx
+++ b/apps/web/app/(dash)/dialogContentContainer.tsx
@@ -100,7 +100,7 @@ export function DialogContentContainer({
}, []);
return (
- <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39] backdrop-blur-md">
+ <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39]">
<form
action={async (e: FormData) => {
const content = e.get("content")?.toString();
diff --git a/apps/web/app/(dash)/header/autoBreadCrumbs.tsx b/apps/web/app/(dash)/header/autoBreadCrumbs.tsx
index a823671c..671464ff 100644
--- a/apps/web/app/(dash)/header/autoBreadCrumbs.tsx
+++ b/apps/web/app/(dash)/header/autoBreadCrumbs.tsx
@@ -13,8 +13,6 @@ import React from "react";
function AutoBreadCrumbs() {
const pathname = usePathname();
- console.log(pathname.split("/").filter(Boolean));
-
return (
<Breadcrumb className="hidden md:block">
<BreadcrumbList>
@@ -31,7 +29,7 @@ function AutoBreadCrumbs() {
.filter(Boolean)
.map((path, idx, paths) => (
<>
- <BreadcrumbItem key={path}>
+ <BreadcrumbItem key={path + idx}>
<BreadcrumbLink href={`/${paths.slice(0, idx + 1).join("/")}`}>
{path.charAt(0).toUpperCase() + path.slice(1)}
</BreadcrumbLink>
diff --git a/apps/web/app/(dash)/header/header.tsx b/apps/web/app/(dash)/header/header.tsx
index b9d400c9..eaade258 100644
--- a/apps/web/app/(dash)/header/header.tsx
+++ b/apps/web/app/(dash)/header/header.tsx
@@ -6,6 +6,14 @@ import Logo from "../../../public/logo.svg";
import { getChatHistory } from "../../actions/fetchers";
import NewChatButton from "./newChatButton";
import AutoBreadCrumbs from "./autoBreadCrumbs";
+import SignOutButton from "./signOutButton";
+import {
+ DropdownMenu,
+ DropdownMenuContent,
+ DropdownMenuItem,
+ DropdownMenuTrigger,
+} from "@repo/ui/shadcn/dropdown-menu";
+import { CaretDownIcon } from "@radix-ui/react-icons";
async function Header() {
const chatThreads = await getChatHistory();
@@ -32,26 +40,28 @@ async function Header() {
<div className="flex items-center gap-2">
<NewChatButton />
- <div className="relative group">
- <button className="flex duration-200 items-center text-[#7D8994] hover:bg-[#1F2429] text-[13px] gap-2 px-3 py-2 rounded-xl">
+ <DropdownMenu>
+ <DropdownMenuTrigger className="inline-flex flex-row flex-nowrap items-center text-muted-foreground hover:text-foreground">
History
- </button>
-
- <div className="absolute p-4 hidden group-hover:block right-0 w-full md:w-[400px] max-h-[70vh] overflow-auto">
- <div className="bg-[#1F2429] rounded-xl p-2 flex flex-col shadow-lg">
- {chatThreads.data.map((thread) => (
+ <CaretDownIcon />
+ </DropdownMenuTrigger>
+ <DropdownMenuContent className="p-4 w-full md:w-[400px] max-h-[70vh] overflow-auto border-none">
+ {chatThreads.data.map((thread) => (
+ <DropdownMenuItem asChild>
<Link
prefetch={false}
href={`/chat/${thread.id}`}
key={thread.id}
- className="p-2 rounded-md hover:bg-secondary"
+ className="p-2 rounded-md cursor-pointer focus:bg-secondary focus:text-current"
>
{thread.firstMessage}
</Link>
- ))}
- </div>
- </div>
- </div>
+ </DropdownMenuItem>
+ ))}
+ </DropdownMenuContent>
+ </DropdownMenu>
+
+ <SignOutButton />
</div>
</div>
</div>
diff --git a/apps/web/app/(dash)/header/signOutButton.tsx b/apps/web/app/(dash)/header/signOutButton.tsx
new file mode 100644
index 00000000..4c61c74d
--- /dev/null
+++ b/apps/web/app/(dash)/header/signOutButton.tsx
@@ -0,0 +1,22 @@
+import { signOut } from "@/server/auth";
+import { Button } from "@repo/ui/shadcn/button";
+
+export default function SignOutButton() {
+ return (
+ <form
+ action={async () => {
+ "use server";
+ await signOut();
+ }}
+ >
+ <Button
+ variant="ghost"
+ size="sm"
+ type="submit"
+ className="text-[#7D8994]"
+ >
+ Sign Out
+ </Button>
+ </form>
+ );
+}
diff --git a/apps/web/app/(dash)/home/history.tsx b/apps/web/app/(dash)/home/history.tsx
index 3d8d5a28..a4cd11d0 100644
--- a/apps/web/app/(dash)/home/history.tsx
+++ b/apps/web/app/(dash)/home/history.tsx
@@ -1,53 +1,65 @@
-import { getChatHistory } from '@repo/web/app/actions/fetchers';
-import { ArrowLongRightIcon } from '@heroicons/react/24/outline';
-import { Skeleton } from '@repo/ui/shadcn/skeleton';
-import Link from 'next/link';
-import { memo, useEffect, useState } from 'react';
-import { motion } from 'framer-motion';
-import { chatThreads } from '@/server/db/schema';
+import { ArrowLongRightIcon } from "@heroicons/react/24/outline";
+import { Skeleton } from "@repo/ui/shadcn/skeleton";
+import { memo, useEffect, useState } from "react";
+import { motion } from "framer-motion";
+import { getQuerySuggestions } from "@/app/actions/doers";
-const History = memo(() => {
- const [chatThreads_, setChatThreads] = useState<
- (typeof chatThreads.$inferSelect)[] | null
- >(null);
+const History = memo(({ setQuery }: { setQuery: (q: string) => void }) => {
+ const [suggestions, setSuggestions] = useState<string[] | null>(null);
- useEffect(() => {
- (async () => {
- const chatThreads = await getChatHistory();
- if (!chatThreads.success || !chatThreads.data) {
- console.error(chatThreads.error);
- return;
- }
- setChatThreads(chatThreads.data.reverse().slice(0, 3));
- })();
- }, []);
+ useEffect(() => {
+ (async () => {
+ const suggestions = await getQuerySuggestions();
+ if (!suggestions.success || !suggestions.data) {
+ console.error(suggestions.error);
+ setSuggestions([]);
+ return;
+ }
+ console.log(suggestions);
+ if (typeof suggestions.data === "string") {
+ const queries = suggestions.data.slice(1, -1).split(", ");
+ const parsedQueries = queries.map((query) =>
+ query.replace(/^'|'$/g, ""),
+ );
+ console.log(parsedQueries);
+ setSuggestions(parsedQueries);
+ return;
+ }
+ setSuggestions(suggestions.data.reverse().slice(0, 3));
+ })();
+ }, []);
- if (!chatThreads) {
- return (
- <>
- <Skeleton className="w-[80%] h-4 bg-[#3b444b] "></Skeleton>
- <Skeleton className="w-[40%] h-4 bg-[#3b444b] "></Skeleton>
- <Skeleton className="w-[60%] h-4 bg-[#3b444b] "></Skeleton>
- </>
- );
- }
-
- return (
- <ul className="text-base list-none space-y-3 text-[#b9b9b9] mt-8">
- {chatThreads_?.map((thread) => (
- <motion.li
- initial={{ opacity: 0, filter: 'blur(1px)' }}
- animate={{ opacity: 1, filter: 'blur(0px)' }}
- className="flex items-center gap-2 truncate"
- >
- <ArrowLongRightIcon className="h-5" />{' '}
- <Link prefetch={false} href={`/chat/${thread.id}`}>
- {thread.firstMessage}
- </Link>
- </motion.li>
- ))}
- </ul>
- );
+ return (
+ <ul className="text-base list-none space-y-3 text-[#b9b9b9] mt-8">
+ {!suggestions && (
+ <>
+ <Skeleton
+ key="loader-1"
+ className="w-[80%] h-4 bg-[#3b444b] "
+ ></Skeleton>
+ <Skeleton
+ key="loader-2"
+ className="w-[40%] h-4 bg-[#3b444b] "
+ ></Skeleton>
+ <Skeleton
+ key="loader-3"
+ className="w-[60%] h-4 bg-[#3b444b] "
+ ></Skeleton>
+ </>
+ )}
+ {suggestions?.map((suggestion) => (
+ <motion.li
+ initial={{ opacity: 0, filter: "blur(1px)" }}
+ animate={{ opacity: 1, filter: "blur(0px)" }}
+ className="flex items-center gap-2 truncate cursor-pointer"
+ key={suggestion}
+ onClick={() => setQuery(suggestion)}
+ >
+ <ArrowLongRightIcon className="h-5" /> {suggestion}
+ </motion.li>
+ ))}
+ </ul>
+ );
});
export default History;
diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx
index cc1856b4..d192d07d 100644
--- a/apps/web/app/(dash)/home/page.tsx
+++ b/apps/web/app/(dash)/home/page.tsx
@@ -4,12 +4,15 @@ import React, { useEffect, useState } from "react";
import QueryInput from "./queryinput";
import { getSessionAuthToken, getSpaces } from "@/app/actions/fetchers";
import { redirect, useRouter } from "next/navigation";
-import { createChatThread, linkTelegramToUser } from "@/app/actions/doers";
+import {
+ createChatThread,
+ getQuerySuggestions,
+ linkTelegramToUser,
+} from "@/app/actions/doers";
import { toast } from "sonner";
import { motion } from "framer-motion";
-import { ChromeIcon, GithubIcon, TwitterIcon } from "lucide-react";
+import { ChromeIcon, GithubIcon, MailIcon, TwitterIcon } from "lucide-react";
import Link from "next/link";
-import { homeSearchParamsCache } from "@/lib/searchParams";
import History from "./history";
const slap = {
@@ -26,28 +29,14 @@ const slap = {
};
function Page({ searchParams }: { searchParams: Record<string, string> }) {
- // TODO: use this to show a welcome page/modal
- const firstTime = searchParams.firstTime === "true";
+ const telegramUser = searchParams.telegramUser;
+ const extensionInstalled = searchParams.extension;
+ const [query, setQuery] = useState(searchParams.q || "");
- const query = searchParams.q || "";
-
- if (firstTime) {
- redirect("/onboarding");
- }
-
- const [queryPresent, setQueryPresent] = useState<boolean>(false);
-
- const [telegramUser, setTelegramUser] = useState<string | undefined>(
- searchParams.telegramUser as string,
- );
- const [extensionInstalled, setExtensionInstalled] = useState<
- string | undefined
- >(searchParams.extension as string);
+ const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
const { push } = useRouter();
- const [spaces, setSpaces] = useState<{ id: number; name: string }[]>([]);
-
useEffect(() => {
if (telegramUser) {
const linkTelegram = async () => {
@@ -63,10 +52,6 @@ function Page({ searchParams }: { searchParams: Record<string, string> }) {
linkTelegram();
}
- if (extensionInstalled) {
- toast.success("Extension installed successfully");
- }
-
getSpaces().then((res) => {
if (res.success && res.data) {
setSpaces(res.data);
@@ -77,21 +62,21 @@ function Page({ searchParams }: { searchParams: Record<string, string> }) {
getSessionAuthToken().then((token) => {
if (typeof window === "undefined") return;
+ if (extensionInstalled) {
+ toast.success("Extension installed successfully");
+ }
window.postMessage({ token: token.data }, "*");
});
}, [telegramUser]);
return (
<div className="max-w-3xl h-full justify-center flex mx-auto w-full flex-col px-2 md:px-0">
- {/* all content goes here */}
- {/* <div className="">hi {firstTime ? 'first time' : ''}</div> */}
-
<motion.h1
{...{
...slap,
transition: { ...slap.transition, delay: 0.2 },
}}
- className="text-center mx-auto bg-[linear-gradient(180deg,_#FFF_0%,_rgba(255,_255,_255,_0.00)_202.08%)] bg-clip-text text-4xl tracking-tighter text-transparent md:text-5xl"
+ className="text-center mx-auto bg-[linear-gradient(180deg,_#FFF_0%,_rgba(255,_255,_255,_0.00)_202.08%)] bg-clip-text text-4xl tracking-tighter text-transparent md:text-5xl pb-2"
>
<span>Ask your</span>{" "}
<span className="inline-flex items-center gap-2 bg-gradient-to-r to-blue-300 from-zinc-300 text-transparent bg-clip-text">
@@ -99,17 +84,16 @@ function Page({ searchParams }: { searchParams: Record<string, string> }) {
</span>
</motion.h1>
- <div className="w-full pb-20 mt-12">
+ <div className="w-full pb-20 mt-10">
<QueryInput
- initialQuery={query}
- setQueryPresent={setQueryPresent}
- handleSubmit={async (q, spaces) => {
+ query={query}
+ setQuery={setQuery}
+ handleSubmit={async (q, spaces, proMode) => {
if (q.length === 0) {
toast.error("Query is required");
return;
}
- console.log("creating thread");
const threadid = await createChatThread(q);
if (!threadid.success || !threadid.data) {
@@ -117,15 +101,14 @@ function Page({ searchParams }: { searchParams: Record<string, string> }) {
return;
}
- console.log("pushing to chat");
push(
- `/chat/${threadid.data}?spaces=${JSON.stringify(spaces)}&q=${q}`,
+ `/chat/${threadid.data}?spaces=${JSON.stringify(spaces)}&q=${q}&proMode=${proMode}`,
);
}}
initialSpaces={spaces}
/>
- <History />
+ <History setQuery={setQuery} />
</div>
<div className="w-full fixed bottom-0 left-0 p-4">
@@ -140,12 +123,12 @@ function Page({ searchParams }: { searchParams: Record<string, string> }) {
Install extension
</Link>
<Link
- href="https://github.com/supermemoryai/supermemory/issues/new"
+ href="mailto:[email protected]"
target="_blank"
rel="noreferrer"
className="flex items-center gap-2 text-muted-foreground hover:text-grey-50 duration-300"
>
- <GithubIcon className="w-4 h-4" />
+ <MailIcon className="w-4 h-4" />
Bug report
</Link>
<Link
diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx
index e49f06e0..82561438 100644
--- a/apps/web/app/(dash)/home/queryinput.tsx
+++ b/apps/web/app/(dash)/home/queryinput.tsx
@@ -1,26 +1,32 @@
"use client";
-import React, { useState } from "react";
+import React, { useEffect, useState } from "react";
import { FilterSpaces } from "./filterSpaces";
import { ArrowRightIcon } from "@repo/ui/icons";
import Image from "next/image";
+import { Switch } from "@repo/ui/shadcn/switch";
+import { Label } from "@repo/ui/shadcn/label";
function QueryInput({
- setQueryPresent,
- initialQuery,
initialSpaces,
handleSubmit,
+ query,
+ setQuery,
}: {
- setQueryPresent: (t: boolean) => void;
initialSpaces?: {
id: number;
name: string;
}[];
- initialQuery?: string;
mini?: boolean;
- handleSubmit: (q: string, spaces: { id: number; name: string }[]) => void;
+ handleSubmit: (
+ q: string,
+ spaces: { id: number; name: string }[],
+ proMode: boolean,
+ ) => void;
+ query: string;
+ setQuery: (q: string) => void;
}) {
- const [q, setQ] = useState(initialQuery || "");
+ const [proMode, setProMode] = useState(false);
const [selectedSpaces, setSelectedSpaces] = useState<
{ id: number; name: string }[]
@@ -34,11 +40,11 @@ function QueryInput({
{/* input and action button */}
<form
action={async () => {
- if (q.trim().length === 0) {
+ if (query.trim().length === 0) {
return;
}
- handleSubmit(q, selectedSpaces);
- setQ("");
+ handleSubmit(query, selectedSpaces, proMode);
+ setQuery("");
}}
>
<textarea
@@ -51,20 +57,15 @@ function QueryInput({
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
- if (q.trim().length === 0) {
+ if (query.trim().length === 0) {
return;
}
- handleSubmit(q, selectedSpaces);
- setQ("");
+ handleSubmit(query, selectedSpaces, proMode);
+ setQuery("");
}
}}
- onChange={(e) =>
- setQ((prev) => {
- setQueryPresent(!!e.target.value.length);
- return e.target.value;
- })
- }
- value={q}
+ onChange={(e) => setQuery(e.target.value)}
+ value={query}
/>
<div className="flex p-2 px-3 w-full items-center justify-between rounded-xl overflow-hidden">
<FilterSpaces
@@ -72,9 +73,22 @@ function QueryInput({
setSelectedSpaces={setSelectedSpaces}
initialSpaces={initialSpaces || []}
/>
- <button type="submit" className="rounded-lg bg-[#369DFD1A] p-3">
- <Image src={ArrowRightIcon} alt="Enter" />
- </button>
+ <div className="flex items-center gap-4">
+ <div className="flex items-center gap-2">
+ <Label htmlFor="pro-mode" className="text-sm text-[#9B9B9B]">
+ Pro mode
+ </Label>
+ <Switch
+ value={proMode ? "on" : "off"}
+ onCheckedChange={(v) => setProMode(v)}
+ id="pro-mode"
+ about="Pro mode"
+ />
+ </div>
+ <button type="submit" className="rounded-lg bg-[#369DFD1A] p-3">
+ <Image src={ArrowRightIcon} alt="Enter" />
+ </button>
+ </div>
</div>
</form>
</div>
diff --git a/apps/web/app/(dash)/layout.tsx b/apps/web/app/(dash)/layout.tsx
index b2b27a4f..c6174945 100644
--- a/apps/web/app/(dash)/layout.tsx
+++ b/apps/web/app/(dash)/layout.tsx
@@ -4,6 +4,7 @@ import { redirect } from "next/navigation";
import { auth } from "../../server/auth";
import { Toaster } from "@repo/ui/shadcn/sonner";
import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid";
+import { getUser } from "../actions/fetchers";
async function Layout({ children }: { children: React.ReactNode }) {
const info = await auth();
@@ -12,6 +13,13 @@ async function Layout({ children }: { children: React.ReactNode }) {
return redirect("/signin");
}
+ const user = await getUser();
+ const hasOnboarded = user.data?.hasOnboarded;
+
+ if (!hasOnboarded) {
+ redirect("/onboarding");
+ }
+
return (
<main className="h-screen flex flex-col">
<div className="fixed top-0 left-0 w-full z-40">
diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx
index 0b487e61..c56a3247 100644
--- a/apps/web/app/(dash)/menu.tsx
+++ b/apps/web/app/(dash)/menu.tsx
@@ -1,176 +1,365 @@
-import React from 'react';
-import Image from 'next/image';
-import Link from 'next/link';
-import { MemoriesIcon, CanvasIcon, AddIcon } from '@repo/ui/icons';
-import { DialogTrigger } from '@repo/ui/shadcn/dialog';
+"use client";
-import { HomeIcon } from '@heroicons/react/24/solid';
+import React, { useEffect, useMemo, useState } from "react";
+import Image from "next/image";
+import Link from "next/link";
import {
- PencilSquareIcon,
- PlusIcon,
- PresentationChartLineIcon,
- RectangleStackIcon,
-} from '@heroicons/react/24/solid';
-import DialogTriggerWrapper, {
- DialogDesktopTrigger,
- DialogMobileTrigger,
-} from './dialogTriggerWrapper';
-
-const menuItems = [
- {
- icon: MemoriesIcon,
- text: 'Memories',
- url: '/memories',
- disabled: false,
- },
- {
- icon: CanvasIcon,
- text: 'Canvas',
- url: '/canvas',
- disabled: true,
- },
-];
-
-const items = [
- {
- icon: <HomeIcon className="h-6 w-6" />,
- name: 'home',
- url: '/home',
- disabled: false,
- },
- {
- icon: <RectangleStackIcon className="h-6 w-6" />,
- name: 'memories',
- url: '/memories',
- disabled: false,
- },
- {
- icon: <PencilSquareIcon className="h-6 w-6" />,
- name: 'editor',
- url: '/#',
- disabled: true,
- },
- {
- icon: <PresentationChartLineIcon className="h-6 w-6" />,
- name: 'thinkpad',
- url: '/#',
- disabled: true,
- },
-];
+ MemoriesIcon,
+ ExploreIcon,
+ CanvasIcon,
+ AddIcon,
+ HomeIcon as HomeIconWeb,
+} from "@repo/ui/icons";
+import { Button } from "@repo/ui/shadcn/button";
+import { MinusIcon, PlusCircleIcon } from "lucide-react";
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogFooter,
+ DialogHeader,
+ DialogTitle,
+ DialogTrigger,
+} from "@repo/ui/shadcn/dialog";
+import { Label } from "@repo/ui/shadcn/label";
+import { Textarea } from "@repo/ui/shadcn/textarea";
+import { toast } from "sonner";
+import { getSpaces } from "../actions/fetchers";
+import { HomeIcon } from "@heroicons/react/24/solid";
+import { createMemory, createSpace } from "../actions/doers";
+import ComboboxWithCreate from "@repo/ui/shadcn/combobox";
+import { StoredSpace } from "@/server/db/schema";
+import useMeasure from "react-use-measure";
function Menu() {
- return (
- <>
- {/* Desktop Menu */}
- <div className="hidden lg:flex items-center pointer-events-none z-[39] fixed left-2 top-0 h-screen flex-col justify-center px-2">
- <div className="pointer-events-none z-10 absolute top-1/2 h-1/3 w-full -translate-y-1/2 bg-secondary blur-[300px] "></div>
- <div className="pointer-events-auto flex flex-col gap-2">
- <DialogDesktopTrigger />
- <div className="inline-flex w-14 flex-col items-start gap-6 rounded-2xl border-[1px] border-gray-700/50 bg-secondary px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]">
- {items.map((v) => (
- <NavItem {...v} />
- ))}
- </div>
- </div>
- </div>
-
- {/* Mobile Menu */}
- <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary z-50 border-t-2 border-border">
- <div className="flex justify-around items-center">
- <Link
- href={'/'}
- className={`flex flex-col items-center text-white ${'cursor-pointer'}`}
- >
- <HomeIcon width={24} height={24} />
- <p className="text-xs text-foreground-menu mt-2">Home</p>
- </Link>
-
- <DialogMobileTrigger />
- {menuItems.map((item) => (
- <Link
- aria-disabled={item.disabled}
- href={item.disabled ? '#' : item.url}
- key={item.url}
- className={`flex flex-col items-center ${
- item.disabled
- ? 'opacity-50 pointer-events-none'
- : 'cursor-pointer'
- }`}
- >
- <Image
- src={item.icon}
- alt={`${item.text} icon`}
- width={24}
- height={24}
- />
- <p className="text-xs text-foreground-menu mt-2">{item.text}</p>
- </Link>
- ))}
- </div>
- </div>
- </>
- );
-}
+ const [spaces, setSpaces] = useState<StoredSpace[]>([]);
-export function Navbar() {
- return (
- <div className="pointer-events-none fixed left-0 top-0 flex h-screen flex-col justify-center px-2">
- <div className="pointer-events-none absolute top-1/2 h-1/3 w-full -translate-y-1/2 bg-blue-500/20 blur-[300px] "></div>
- <div className="pointer-events-auto">
- <div className="inline-flex w-14 flex-col items-start gap-6 rounded-2xl border-2 border-border px-3 py-4 text-[#b9b9b9] shadow-md shadow-[#1d1d1dc7]">
- <Top />
- {items.map((v) => (
- <NavItem {...v} />
- ))}
- </div>
- </div>
- </div>
- );
-}
+ useEffect(() => {
+ (async () => {
+ let spaces = await getSpaces();
-function Top() {
- return (
- <DialogTriggerWrapper>
- <DialogTrigger>
- <div className="space-y-4 group relative">
- <div className="cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90">
- <PlusIcon className="h-6 w-6" />
- </div>
- <div className="h-[1px] w-full bg-[#323b41]"></div>
- <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap -top-1 -translate-y-1/2 left-[150%] pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-2xl px-2 py-1">
- Add Memories
- </div>
- </div>
- </DialogTrigger>
- </DialogTriggerWrapper>
- );
-}
+ if (!spaces.success || !spaces.data) {
+ toast.warning("Unable to get spaces", {
+ richColors: true,
+ });
+ setSpaces([]);
+ return;
+ }
+ setSpaces(spaces.data);
+ })();
+ }, []);
+
+ const menuItems = [
+ {
+ icon: HomeIconWeb,
+ text: "Home",
+ url: "/home",
+ disabled: false,
+ },
+ {
+ icon: MemoriesIcon,
+ text: "Memories",
+ url: "/memories",
+ disabled: false,
+ },
+ ];
+
+ const [content, setContent] = useState("");
+ const [selectedSpaces, setSelectedSpaces] = useState<number[]>([]);
+
+ const autoDetectedType = useMemo(() => {
+ if (content.length === 0) {
+ return "none";
+ }
+
+ if (
+ content.match(/https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/)
+ ) {
+ return "tweet";
+ } else if (content.match(/https?:\/\/[\w\.]+/)) {
+ return "page";
+ } else if (content.match(/https?:\/\/www\.[\w\.]+/)) {
+ return "page";
+ } else {
+ return "note";
+ }
+ }, [content]);
+
+ const [dialogOpen, setDialogOpen] = useState(false);
+
+ const options = useMemo(
+ () =>
+ spaces.map((x) => ({
+ label: x.name,
+ value: x.id.toString(),
+ })),
+ [spaces],
+ );
+
+ const handleSubmit = async (content?: string, spaces?: number[]) => {
+ setDialogOpen(false);
+
+ toast.info("Creating memory...", {
+ icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />,
+ duration: 7500,
+ });
+
+ if (!content || content.length === 0) {
+ toast.error("Content is required");
+ return;
+ }
+
+ console.log(spaces);
+
+ const cont = await createMemory({
+ content: content,
+ spaces: spaces ?? undefined,
+ });
+
+ setContent("");
+ setSelectedSpaces([]);
+
+ if (cont.success) {
+ toast.success("Memory created", {
+ richColors: true,
+ });
+ } else {
+ toast.error(`Memory creation failed: ${cont.error}`);
+ }
+ };
+
+ return (
+ <>
+ {/* Desktop Menu */}
+ <Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
+ <div className="hidden lg:flex fixed h-screen pb-20 w-full p-4 items-center justify-start top-0 left-0 pointer-events-none z-[39]">
+ <div className="pointer-events-auto group flex w-14 text-foreground-menu text-[15px] font-medium flex-col items-start gap-6 overflow-hidden rounded-[28px] border-2 border-border bg-secondary px-3 py-4 duration-200 hover:w-40 z-[99999]">
+ <div className="border-b border-border pb-4 w-full">
+ <DialogTrigger
+ className={`flex w-full text-white brightness-75 hover:brightness-125 focus:brightness-125 cursor-pointer items-center gap-3 px-1 duration-200 justify-start`}
+ >
+ <Image
+ src={AddIcon}
+ alt="Logo"
+ width={24}
+ height={24}
+ className="hover:brightness-125 focus:brightness-125 duration-200 text-white"
+ />
+ <p className="opacity-0 duration-200 group-hover:opacity-100">
+ Add
+ </p>
+ </DialogTrigger>
+ </div>
+ {menuItems.map((item) => (
+ <Link
+ aria-disabled={item.disabled}
+ href={item.disabled ? "#" : item.url}
+ key={item.url}
+ className={`flex w-full ${
+ item.disabled
+ ? "cursor-not-allowed opacity-30"
+ : "text-white brightness-75 hover:brightness-125 cursor-pointer"
+ } items-center gap-3 px-1 duration-200 hover:scale-105 active:scale-90 justify-start`}
+ >
+ <Image
+ src={item.icon}
+ alt={`${item.text} icon`}
+ width={24}
+ height={24}
+ className="hover:brightness-125 duration-200"
+ />
+ <p className="opacity-0 duration-200 group-hover:opacity-100">
+ {item.text}
+ </p>
+ </Link>
+ ))}
+ </div>
+ </div>
+
+ <DialogContent className="sm:max-w-[475px] text-[#F2F3F5] rounded-2xl bg-background z-[39]">
+ <form
+ action={async (e: FormData) => {
+ const content = e.get("content")?.toString();
+
+ await handleSubmit(content, selectedSpaces);
+ }}
+ className="flex flex-col gap-4 "
+ >
+ <DialogHeader>
+ <DialogTitle>Add memory</DialogTitle>
+ <DialogDescription className="text-[#F2F3F5]">
+ A "Memory" is a bookmark, something you want to remember.
+ </DialogDescription>
+ </DialogHeader>
+
+ <div>
+ <Label htmlFor="name">Resource (URL or content)</Label>
+ <Textarea
+ className={`bg-[#2F353C] text-[#DBDEE1] max-h-[35vh] overflow-auto focus-visible:ring-0 border-none focus-visible:ring-offset-0 mt-2 ${/^https?:\/\/\S+$/i.test(content) && "text-[#1D9BF0] underline underline-offset-2"}`}
+ id="content"
+ name="content"
+ rows={8}
+ placeholder="Start typing a note or paste a URL here. I'll remember it."
+ value={content}
+ onChange={(e) => setContent(e.target.value)}
+ onKeyDown={(e) => {
+ if (e.key === "Enter" && !e.shiftKey) {
+ e.preventDefault();
+ handleSubmit(content, selectedSpaces);
+ }
+ }}
+ />
+ </div>
+
+ <div>
+ <Label className="space-y-1" htmlFor="space">
+ <h3 className="font-semibold text-lg tracking-tight">
+ Spaces (Optional)
+ </h3>
+ <p className="leading-normal text-[#F2F3F5] text-sm">
+ A space is a collection of memories. It's a way to organise
+ your memories.
+ </p>
+ </Label>
+
+ <ComboboxWithCreate
+ options={spaces.map((x) => ({
+ label: x.name,
+ value: x.id.toString(),
+ }))}
+ onSelect={(v) =>
+ setSelectedSpaces((prev) => {
+ if (v === "") {
+ return [];
+ }
+ return [...prev, parseInt(v)];
+ })
+ }
+ onSubmit={async (spaceName) => {
+ const space = options.find((x) => x.label === spaceName);
+ toast.info("Creating space...");
+
+ if (space) {
+ toast.error("A space with that name already exists.");
+ }
+
+ const creationTask = await createSpace(spaceName);
+ if (creationTask.success && creationTask.data) {
+ toast.success("Space created " + creationTask.data);
+ setSpaces((prev) => [
+ ...prev,
+ {
+ name: spaceName,
+ id: creationTask.data!,
+ createdAt: new Date(),
+ user: null,
+ numItems: 0,
+ },
+ ]);
+ setSelectedSpaces((prev) => [...prev, creationTask.data!]);
+ } else {
+ toast.error(
+ "Space creation failed: " + creationTask.error ??
+ "Unknown error",
+ );
+ }
+ }}
+ placeholder="Select or create a new space."
+ className="bg-[#2F353C] h-min rounded-md mt-4 mb-4"
+ />
+
+ <div>
+ {selectedSpaces.length > 0 && (
+ <div className="flex flex-row flex-wrap gap-0.5 h-min">
+ {[...new Set(selectedSpaces)].map((x, idx) => (
+ <button
+ key={x}
+ type="button"
+ onClick={() =>
+ setSelectedSpaces((prev) =>
+ prev.filter((y) => y !== x),
+ )
+ }
+ className={`relative group p-2 py-3 bg-[#3C464D] max-w-32 ${
+ idx === selectedSpaces.length - 1
+ ? "rounded-br-xl"
+ : ""
+ }`}
+ >
+ <p className="line-clamp-1">
+ {spaces.find((y) => y.id === x)?.name}
+ </p>
+ <div className="absolute h-full right-0 top-0 p-1 opacity-0 group-hover:opacity-100 items-center">
+ <MinusIcon className="w-6 h-6 rounded-full bg-secondary" />
+ </div>
+ </button>
+ ))}
+ </div>
+ )}
+ </div>
+ </div>
+
+ <DialogFooter>
+ <Button
+ disabled={autoDetectedType === "none"}
+ variant={"secondary"}
+ type="submit"
+ >
+ Save {autoDetectedType != "none" && autoDetectedType}
+ </Button>
+ </DialogFooter>
+ </form>
+ </DialogContent>
+
+ {/* Mobile Menu */}
+ <div className="lg:hidden fixed bottom-0 left-0 w-full p-4 bg-secondary z-50 border-t-2 border-border">
+ <div className="flex justify-around items-center">
+ <Link
+ href={"/"}
+ className={`flex flex-col items-center text-white ${"cursor-pointer"}`}
+ >
+ <HomeIcon width={24} height={24} />
+ <p className="text-xs text-foreground-menu mt-2">Home</p>
+ </Link>
-function NavItem({
- disabled,
- icon,
- url,
- name,
-}: {
- disabled: boolean;
- icon: React.JSX.Element;
- name: string;
- url: string;
-}) {
- return (
- <div className="relative group">
- <Link aria-disabled={disabled} href={disabled ? '#' : url}>
- <div
- className={`cursor-pointer px-1 hover:scale-105 hover:text-[#bfc4c9] active:scale-90 ${disabled && 'opacity-50'}`}
- >
- {icon}
- </div>
- </Link>
- <div className="opacity-0 group-hover:opacity-100 scale-x-50 group-hover:scale-x-100 origin-left transition-all absolute whitespace-nowrap top-1/2 -translate-y-1/2 left-[150%] pointer-events-none border-gray-700/50 border-[1px] bg-[#1F2428] shadow-md shadow-[#1d1d1dc7] rounded-xl px-2 py-1">
- {name}
- </div>
- </div>
- );
+ <DialogTrigger
+ className={`flex flex-col items-center cursor-pointer text-white`}
+ >
+ <Image
+ src={AddIcon}
+ alt="Logo"
+ width={24}
+ height={24}
+ className="hover:brightness-125 focus:brightness-125 duration-200 stroke-white"
+ />
+ <p className="text-xs text-foreground-menu mt-2">Add</p>
+ </DialogTrigger>
+ {menuItems.slice(1, 2).map((item) => (
+ <Link
+ aria-disabled={item.disabled}
+ href={item.disabled ? "#" : item.url}
+ key={item.url}
+ className={`flex flex-col items-center ${
+ item.disabled
+ ? "opacity-50 cursor-not-allowed"
+ : "cursor-pointer"
+ }`}
+ onClick={(e) => item.disabled && e.preventDefault()}
+ >
+ <Image
+ src={item.icon}
+ alt={`${item.text} icon`}
+ width={24}
+ height={24}
+ />
+ <p className="text-xs text-foreground-menu mt-2">{item.text}</p>
+ </Link>
+ ))}
+ </div>
+ </div>
+ </Dialog>
+ </>
+ );
}
export default Menu;
diff --git a/apps/web/app/(landing)/Hero.tsx b/apps/web/app/(landing)/Hero.tsx
index 4a9a8e04..1cdfced8 100644
--- a/apps/web/app/(landing)/Hero.tsx
+++ b/apps/web/app/(landing)/Hero.tsx
@@ -53,7 +53,7 @@ function Hero() {
</motion.p>
<Link
href="/signin"
- className="inline-flex text-lg gap-x-2 mt-2 backdrop-blur-md text-white justify-center items-center py-3 px-5 ml-3 w-fit rounded-3xl border duration-200 group bg-page-gradient border-white/30 text-md font-geistSans hover:border-zinc-600 hover:bg-transparent/10 hover:text-zinc-100"
+ className="inline-flex text-lg gap-x-2 mt-2 backdrop-blur-md text-white justify-center items-center py-3 px-5 w-fit rounded-3xl border duration-200 group bg-page-gradient border-white/30 text-md font-geistSans hover:border-zinc-600 hover:bg-transparent/10 hover:text-zinc-100"
>
It's free. Sign up now
<div className="flex overflow-hidden relative justify-center items-center ml-1 w-5 h-5">
@@ -61,22 +61,38 @@ function Hero() {
<ArrowUpRight className="absolute transition-all duration-500 -translate-x-4 -translate-y-5 group-hover:translate-x-0 group-hover:translate-y-0" />
</div>
</Link>
+ <a
+ href="https://www.producthunt.com/posts/supermemory?embed=true&utm_source=badge-top-post-badge&utm_medium=badge&utm_souce=badge-supermemory"
+ target="_blank"
+ >
+ <img
+ src="https://api.producthunt.com/widgets/embed-image/v1/top-post-badge.svg?post_id=472686&theme=dark&period=daily"
+ alt="Supermemory - AI&#0032;second&#0032;brain&#0032;for&#0032;all&#0032;your&#0032;saved&#0032;stuff | Product Hunt"
+ style={{ width: "250px", height: "54px" }}
+ width="250"
+ height="54"
+ />
+ </a>
</section>
<AnimatedLogoCloud />
<div className="relative z-50">
- <motion.img
+ <motion.iframe
{...{
...slap,
transition: { ...slap.transition, delay: 0.35 },
}}
- src="/images/landing-hero.jpeg"
- alt="Landing page background"
draggable="false"
- className="z-40 md:mt-[-40px] hidden sm:block h-full max-w-[70vw] mx-auto md:w-full select-none px-5 !rounded-2xl"
+ className="z-40 relative md:mt-[-40px] hidden sm:block h-full max-w-[70vw] mx-auto md:w-full select-none px-5 !rounded-2xl"
style={{
borderRadius: "20px",
+ width: "100%",
+ height: "100%",
}}
+ src="https://customer-5xczlbkyq4f9ejha.cloudflarestream.com/111c4828c3587348bc703e67bfca9682/iframe?preload=true&poster=https%3A%2F%2Fcustomer-5xczlbkyq4f9ejha.cloudflarestream.com%2F111c4828c3587348bc703e67bfca9682%2Fthumbnails%2Fthumbnail.jpg%3Ftime%3D%26height%3D600"
+ loading="lazy"
+ allow="accelerometer; gyroscope; autoplay; encrypted-media; picture-in-picture;"
+ allowFullScreen={true}
/>
<div
className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 blur-[337.4px]"
diff --git a/apps/web/app/(quicklinks)/ph/route.ts b/apps/web/app/(quicklinks)/ph/route.ts
new file mode 100644
index 00000000..7355c24c
--- /dev/null
+++ b/apps/web/app/(quicklinks)/ph/route.ts
@@ -0,0 +1,5 @@
+import { redirect } from "next/navigation";
+
+export async function GET() {
+ return redirect("https://www.producthunt.com/posts/supermemory");
+}
diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts
index f3677f4c..da2bfb5f 100644
--- a/apps/web/app/actions/doers.ts
+++ b/apps/web/app/actions/doers.ts
@@ -22,6 +22,34 @@ import { ChatHistory } from "@repo/shared-types";
import { decipher } from "@/server/encrypt";
import { redirect } from "next/navigation";
import { tweetToMd } from "@repo/shared-types/utils";
+import { ensureAuth } from "../api/ensureAuth";
+import { getRandomSentences } from "@/lib/utils";
+import { getRequestContext } from "@cloudflare/next-on-pages";
+
+export const completeOnboarding = async (): ServerActionReturnType<boolean> => {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ try {
+ const res = await db
+ .update(users)
+ .set({ hasOnboarded: true })
+ .where(eq(users.id, data.user.id))
+ .returning({ hasOnboarded: users.hasOnboarded });
+
+ if (res.length === 0 || !res[0]?.hasOnboarded) {
+ return { success: false, data: false, error: "Failed to update user" };
+ }
+
+ return { success: true, data: res[0].hasOnboarded };
+ } catch (e) {
+ return { success: false, data: false, error: (e as Error).message };
+ }
+};
export const createSpace = async (
input: string | FormData,
@@ -468,6 +496,7 @@ export const createChatObject = async (
answer: lastChat.answer.parts.map((part) => part.text).join(""),
answerSources: JSON.stringify(lastChat.answer.sources),
threadId,
+ createdAt: new Date(),
});
if (!saved) {
@@ -744,3 +773,122 @@ export async function AddCanvasInfo({
};
}
}
+
+export async function getQuerySuggestions() {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ const { env } = getRequestContext();
+
+ try {
+ const recommendations = await env.RECOMMENDATIONS.get(data.user.id);
+
+ if (recommendations) {
+ return {
+ success: true,
+ data: JSON.parse(recommendations),
+ };
+ }
+
+ // Randomly choose some storedContent of the user.
+ const content = await db
+ .select()
+ .from(storedContent)
+ .where(eq(storedContent.userId, data.user.id))
+ .orderBy(sql`random()`)
+ .limit(5)
+ .all();
+
+ if (content.length === 0) {
+ return {
+ success: true,
+ data: [],
+ };
+ }
+
+ const fullQuery = content
+ .map((c) => `${c.title} \n\n${c.content}`)
+ .join(" ");
+
+ const suggestionsCall = (await env.AI.run(
+ // @ts-ignore
+ "@cf/meta/llama-3.1-8b-instruct",
+ {
+ messages: [
+ {
+ role: "system",
+ content: `You are a model that suggests questions based on the user's content. you MUST suggest atleast 1 question to ask. AT MAX, create 3 suggestions. not more than that.`,
+ },
+ {
+ role: "user",
+ content: `Run the function based on this input: ${fullQuery.slice(0, 2000)}`,
+ },
+ ],
+ tools: [
+ {
+ type: "function",
+ function: {
+ name: "querySuggestions",
+ description:
+ "Take the user's content to suggest some good questions that they could ask.",
+ parameters: {
+ type: "object",
+ properties: {
+ querySuggestions: {
+ type: "array",
+ description:
+ "Short questions that the user can ask. Give atleast 3 suggestions. No more than 5.",
+ items: {
+ type: "string",
+ },
+ },
+ },
+ required: ["querySuggestions"],
+ },
+ },
+ },
+ ],
+ },
+ )) as {
+ response: string;
+ tool_calls: { name: string; arguments: { querySuggestions: string[] } }[];
+ };
+
+ console.log(
+ "I RAN AN AI CALLS OWOWOWOWOW",
+ JSON.stringify(suggestionsCall, null, 2),
+ );
+
+ const suggestions =
+ suggestionsCall.tool_calls?.[0]?.arguments?.querySuggestions;
+
+ if (!suggestions || suggestions.length === 0) {
+ return {
+ success: false,
+ error: "Failed to get query suggestions",
+ };
+ }
+
+ if (suggestions.length > 0) {
+ await env.RECOMMENDATIONS.put(data.user.id, JSON.stringify(suggestions), {
+ expirationTtl: 60 * 2,
+ });
+ }
+
+ return {
+ success: true,
+ data: suggestions,
+ };
+ } catch (exception) {
+ const error = exception as Error;
+ return {
+ success: false,
+ error: error.message,
+ data: [],
+ };
+ }
+}
diff --git a/apps/web/app/actions/fetchers.ts b/apps/web/app/actions/fetchers.ts
index 7071ecb3..8d6802a7 100644
--- a/apps/web/app/actions/fetchers.ts
+++ b/apps/web/app/actions/fetchers.ts
@@ -1,6 +1,6 @@
"use server";
-import { and, asc, eq, exists, inArray, not, or, sql } from "drizzle-orm";
+import { and, asc, eq, exists, not, or } from "drizzle-orm";
import { db } from "../../server/db";
import {
canvas,
@@ -13,15 +13,32 @@ import {
spacesAccess,
storedContent,
StoredSpace,
+ User,
users,
} from "../../server/db/schema";
-import { ServerActionReturnType, Space } from "./types";
+import { ServerActionReturnType } from "./types";
import { auth } from "../../server/auth";
import { ChatHistory, SourceZod } from "@repo/shared-types";
import { z } from "zod";
import { redirect } from "next/navigation";
import { cookies, headers } from "next/headers";
+export const getUser = async (): ServerActionReturnType<User> => {
+ const data = await auth();
+
+ if (!data || !data.user || !data.user.id) {
+ redirect("/signin");
+ return { error: "Not authenticated", success: false };
+ }
+
+ console.log("data.user.id", data.user.id);
+ const user = await db.query.users.findFirst({
+ where: eq(users.id, data.user.id),
+ });
+
+ return { success: true, data: user };
+};
+
export const getSpaces = async (): ServerActionReturnType<StoredSpace[]> => {
const data = await auth();
@@ -218,6 +235,9 @@ export const getFullChatThread = async (
],
sources: sources ?? [],
},
+ proModeProcessing: {
+ queries: [],
+ },
};
},
);
diff --git a/apps/web/app/api/chat/route.ts b/apps/web/app/api/chat/route.ts
index 004bfd3b..a14c96df 100644
--- a/apps/web/app/api/chat/route.ts
+++ b/apps/web/app/api/chat/route.ts
@@ -7,6 +7,10 @@ import {
} from "@repo/shared-types";
import { ensureAuth } from "../ensureAuth";
import { z } from "zod";
+import { db } from "@/server/db";
+import { chatHistory as chatHistoryDb, chatThreads } from "@/server/db/schema";
+import { and, eq, gt, sql } from "drizzle-orm";
+import { join } from "path";
export const runtime = "edge";
@@ -21,12 +25,56 @@ export async function POST(req: NextRequest) {
return new Response("Missing BACKEND_SECURITY_KEY", { status: 500 });
}
+ const ip = req.headers.get("cf-connecting-ip");
+
+ if (ip) {
+ if (process.env.RATELIMITER) {
+ const { success } = await process.env.RATELIMITER.limit({
+ key: `chat-${ip}`,
+ });
+
+ if (!success) {
+ console.error("rate limit exceeded");
+ return new Response("Rate limit exceeded", { status: 429 });
+ }
+ } else {
+ console.info("RATELIMITER not found in env");
+ }
+ } else {
+ console.info("cf-connecting-ip not found in headers");
+ }
+
+ const lastHour = new Date(new Date().getTime() - 3600000);
+
+ // Only allow 5 requests per hour for each user, something lke this but this one is bad because chathistory.userid doesnt exist, we have to do a join and get it from the threads table
+ const result = await db
+ .select({
+ count: sql<number>`count(*)`.mapWith(Number),
+ })
+ .from(chatHistoryDb)
+ .innerJoin(chatThreads, eq(chatHistoryDb.threadId, chatThreads.id))
+ .where(
+ and(
+ eq(chatThreads.userId, session.user.id),
+ gt(chatHistoryDb.createdAt, lastHour),
+ ),
+ )
+ .execute();
+
+ if (result[0]?.count && result[0]?.count >= 5) {
+ // return new Response(`Too many requests ${result[0]?.count}`, { status: 429 });
+ console.log(result[0]?.count);
+ } else {
+ console.log("count", result);
+ }
+
const url = new URL(req.url);
const query = url.searchParams.get("q");
const spaces = url.searchParams.get("spaces");
const sourcesOnly = url.searchParams.get("sourcesOnly") ?? "false";
+ const proMode = url.searchParams.get("proMode") === "true";
const jsonRequest = (await req.json()) as {
chatHistory: ChatHistory[];
@@ -55,7 +103,7 @@ export async function POST(req: NextRequest) {
const modelCompatible = await convertChatHistoryList(validated.data);
const resp = await fetch(
- `${process.env.BACKEND_BASE_URL}/api/chat?query=${query}&user=${session.user.id}&sourcesOnly=${sourcesOnly}&spaces=${spaces}`,
+ `${process.env.BACKEND_BASE_URL}/api/chat?query=${query}&user=${session.user.id}&sourcesOnly=${sourcesOnly}&spaces=${spaces}&proMode=${proMode}`,
{
headers: {
Authorization: `Bearer ${process.env.BACKEND_SECURITY_KEY}`,
diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts
index f9ab7c01..992c2a0e 100644
--- a/apps/web/app/api/store/route.ts
+++ b/apps/web/app/api/store/route.ts
@@ -4,7 +4,7 @@ import { ensureAuth } from "../ensureAuth";
import { z } from "zod";
import { db } from "@/server/db";
import { contentToSpace, space, storedContent } from "@/server/db/schema";
-import { and, eq, inArray } from "drizzle-orm";
+import { and, eq, gt, inArray, sql } from "drizzle-orm";
import { LIMITS } from "@/lib/constants";
import { limit } from "@/app/actions/doers";
@@ -22,6 +22,29 @@ const createMemoryFromAPI = async (input: {
};
}
+ // Get number of items saved in the last 2 hours
+ const last2Hours = new Date(Date.now() - 2 * 60 * 60 * 1000);
+
+ const numberOfItemsSavedInLast2Hours = await db
+ .select({
+ count: sql<number>`count(*)`.mapWith(Number),
+ })
+ .from(storedContent)
+ .where(
+ and(
+ gt(storedContent.savedAt, last2Hours),
+ eq(storedContent.userId, input.userId),
+ ),
+ );
+
+ if (numberOfItemsSavedInLast2Hours[0]!.count >= 20) {
+ return {
+ success: false,
+ data: 0,
+ error: `You have exceeded the limit`,
+ };
+ }
+
const vectorSaveResponse = await fetch(
`${process.env.BACKEND_BASE_URL}/api/add`,
{
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
index cf6e9b0f..94a538ed 100644
--- a/apps/web/app/layout.tsx
+++ b/apps/web/app/layout.tsx
@@ -66,9 +66,14 @@ export default function RootLayout({
}): JSX.Element {
return (
<html lang="en" className="overflow-x-hidden" suppressHydrationWarning>
- {/* <head>
- <ThemeScript />
- </head> */}
+ <head>
+ {/* Cloudflare web analytics */}
+ <script
+ defer
+ src="https://static.cloudflareinsights.com/beacon.min.js"
+ data-cf-beacon='{"token": "16d76ebb82c74d9983b71d09ab6617bc"}'
+ ></script>
+ </head>
{/* TODO: when lightmode support is added, remove the 'dark' class from the body tag */}
<body
className={cn(
diff --git a/apps/web/cf-env.d.ts b/apps/web/cf-env.d.ts
index 2c77d4fb..09b0690b 100644
--- a/apps/web/cf-env.d.ts
+++ b/apps/web/cf-env.d.ts
@@ -22,6 +22,10 @@ declare global {
THREAD_CF_AUTH: string;
MOBILE_TRUST_TOKEN: string;
+
+ RATELIMITER: {
+ limit: ({ key: string }) => { success: boolean };
+ };
}
}
}
diff --git a/apps/web/env.d.ts b/apps/web/env.d.ts
index b6a410f9..c80ac0a4 100644
--- a/apps/web/env.d.ts
+++ b/apps/web/env.d.ts
@@ -6,4 +6,6 @@ interface CloudflareEnv {
DATABASE: D1Database;
DEV_IMAGES: R2Bucket;
CANVAS_SNAPS: KVNamespace;
+ AI: Ai;
+ RECOMMENDATIONS: KVNamespace;
}
diff --git a/apps/web/lib/searchParams.ts b/apps/web/lib/searchParams.ts
index 2e8b1633..b90b560c 100644
--- a/apps/web/lib/searchParams.ts
+++ b/apps/web/lib/searchParams.ts
@@ -32,4 +32,5 @@ export const chatSearchParamsCache = createSearchParamsCache({
return valid.data;
}),
+ proMode: parseAsBoolean.withDefault(false),
});
diff --git a/apps/web/lib/utils.ts b/apps/web/lib/utils.ts
new file mode 100644
index 00000000..98ec6e98
--- /dev/null
+++ b/apps/web/lib/utils.ts
@@ -0,0 +1,23 @@
+export function getRandomSentences(fullQuery: string): string {
+ // Split the fullQuery into sentences
+ const sentences = fullQuery.match(/[^.!?]+[.!?]+/g) || [];
+
+ // Function to get a random integer between min and max
+ function getRandomInt(min: number, max: number): number {
+ return Math.floor(Math.random() * (max - min)) + min;
+ }
+
+ let selectedSentences = "";
+ let totalCharacters = 0;
+
+ // Select random sentences until totalCharacters is at least 1000
+ while (totalCharacters < 1000 && sentences.length > 0) {
+ const randomIndex = getRandomInt(0, sentences.length);
+ const sentence = sentences[randomIndex];
+ selectedSentences += sentence;
+ totalCharacters += sentence?.length || 0;
+ sentences.splice(randomIndex, 1); // Remove the selected sentence from the array
+ }
+
+ return selectedSentences;
+}
diff --git a/apps/web/migrations/0000_exotic_sway.sql b/apps/web/migrations/0000_steep_moira_mactaggert.sql
index 65a41795..5813639d 100644
--- a/apps/web/migrations/0000_exotic_sway.sql
+++ b/apps/web/migrations/0000_steep_moira_mactaggert.sql
@@ -43,6 +43,7 @@ CREATE TABLE `chatHistory` (
`answerParts` text,
`answerSources` text,
`answerJustification` text,
+ `createdAt` integer DEFAULT '"2024-07-25T22:31:50.848Z"' NOT NULL,
FOREIGN KEY (`threadId`) REFERENCES `chatThread`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
@@ -106,7 +107,8 @@ CREATE TABLE `user` (
`email` text NOT NULL,
`emailVerified` integer,
`image` text,
- `telegramId` text
+ `telegramId` text,
+ `hasOnboarded` integer DEFAULT false
);
--> statement-breakpoint
CREATE TABLE `verificationToken` (
diff --git a/apps/web/migrations/meta/0000_snapshot.json b/apps/web/migrations/meta/0000_snapshot.json
index 0639eb77..a7689010 100644
--- a/apps/web/migrations/meta/0000_snapshot.json
+++ b/apps/web/migrations/meta/0000_snapshot.json
@@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
- "id": "ab91d972-05ff-4916-84b7-1cfaab4c3879",
+ "id": "8705302a-eae7-4fbf-9ce8-8ae23df228a2",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"account": {
@@ -298,6 +298,14 @@
"primaryKey": false,
"notNull": false,
"autoincrement": false
+ },
+ "createdAt": {
+ "name": "createdAt",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": true,
+ "autoincrement": false,
+ "default": "'\"2024-07-25T22:31:50.848Z\"'"
}
},
"indexes": {
@@ -738,6 +746,14 @@
"primaryKey": false,
"notNull": false,
"autoincrement": false
+ },
+ "hasOnboarded": {
+ "name": "hasOnboarded",
+ "type": "integer",
+ "primaryKey": false,
+ "notNull": false,
+ "autoincrement": false,
+ "default": false
}
},
"indexes": {
diff --git a/apps/web/migrations/meta/_journal.json b/apps/web/migrations/meta/_journal.json
index c7ab51e1..d79e2607 100644
--- a/apps/web/migrations/meta/_journal.json
+++ b/apps/web/migrations/meta/_journal.json
@@ -5,8 +5,8 @@
{
"idx": 0,
"version": "6",
- "when": 1720360287793,
- "tag": "0000_exotic_sway",
+ "when": 1721946710900,
+ "tag": "0000_steep_moira_mactaggert",
"breakpoints": true
}
]
diff --git a/apps/web/server/db/index.ts b/apps/web/server/db/index.ts
index 4d671bea..a9ec9106 100644
--- a/apps/web/server/db/index.ts
+++ b/apps/web/server/db/index.ts
@@ -2,4 +2,7 @@ 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: process.env.NODE_ENV === "development",
+});
diff --git a/apps/web/server/db/schema.ts b/apps/web/server/db/schema.ts
index ae293a91..32b80719 100644
--- a/apps/web/server/db/schema.ts
+++ b/apps/web/server/db/schema.ts
@@ -1,3 +1,4 @@
+import { create } from "domain";
import { relations, sql } from "drizzle-orm";
import {
index,
@@ -22,6 +23,7 @@ export const users = createTable(
emailVerified: integer("emailVerified", { mode: "timestamp_ms" }),
image: text("image"),
telegramId: text("telegramId"),
+ hasOnboarded: integer("hasOnboarded", { mode: "boolean" }).default(false),
},
(user) => ({
emailIdx: index("users_email_idx").on(user.email),
@@ -210,6 +212,9 @@ export const chatHistory = createTable(
answer: text("answerParts"), // Single answer part as string
answerSources: text("answerSources"), // JSON stringified array of objects
answerJustification: text("answerJustification"),
+ createdAt: int("createdAt", { mode: "timestamp" })
+ .notNull()
+ .default(new Date()),
},
(history) => ({
threadIdx: index("chatHistory_thread_idx").on(history.threadId),
diff --git a/apps/web/wrangler.toml b/apps/web/wrangler.toml
index ce38285b..7f3fa047 100644
--- a/apps/web/wrangler.toml
+++ b/apps/web/wrangler.toml
@@ -3,6 +3,20 @@ compatibility_date = "2024-03-29"
compatibility_flags = [ "nodejs_compat" ]
pages_build_output_dir = ".vercel/output/static"
+
+kv_namespaces = [
+ { binding = "CANVAS_SNAPS", id = "6df98c892b3744ccb0c631d9f60d6697" },
+ { binding = "RECOMMENDATIONS", id = "83bc7055226c4657948141c2ff9a5425" }
+]
+
+env.production.kv_namespaces = [
+ { binding = "CANVAS_SNAPS", id = "6df98c892b3744ccb0c631d9f60d6697" },
+ { binding = "RECOMMENDATIONS", id = "83bc7055226c4657948141c2ff9a5425" }
+]
+
+[ai]
+binding = "AI"
+
[placement]
mode = "smart"
@@ -15,16 +29,19 @@ binding = "DATABASE"
database_name = "dev-d1-anycontext"
database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c"
-[[kv_namespaces]]
-binding = "CANVAS_SNAPS"
-id = "c6446f7190dd4afebe1c318df3400518"
[[env.production.d1_databases]]
binding = "DATABASE"
database_name = "prod-d1-supermemory"
database_id = "f527a727-c472-41d4-8eaf-3d7ba0f2f395"
+[env.preview.ai]
+binding = "AI"
+
[[env.preview.d1_databases]]
binding = "DATABASE"
database_name = "dev-d1-anycontext"
-database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c" \ No newline at end of file
+database_id = "fc562605-157a-4f60-b439-2a24ffed5b4c"
+
+[env.production.ai]
+binding = "AI" \ No newline at end of file