merged latest changes with queue branch and ready for prod

This commit is contained in:
Dhravya Shah 2024-08-06 11:05:02 -07:00
commit be3e13b4bf
28 changed files with 1311 additions and 298 deletions

View file

@ -21,7 +21,7 @@ Interested in helping build the best second brain for everyone? Join the discord
## 👀 What is this?
Build your own second brain with supermemory. It's a ChatGPT for your bookmarks. Import tweets or save websites and content using the [chrome extension](https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?hl=en-GB&authuser=0)
Build your own second brain with supermemory. It's a ChatGPT for your bookmarks. Import tweets or save websites and content using the [Chrome extension](https://chromewebstore.google.com/detail/supermemory/afpgkkipfdpeaflnpoaffkcankadgjfc?hl=en-GB&authuser=0)
Well, here's the thing - me and @yxshv save a _lot_ of content on the internet.
@ -66,7 +66,7 @@ To use the chrome extension,
<img width="480" alt="image" src="https://github.com/MaheshtheDev/supermemory/assets/38828053/2efb06a5-912a-48e7-ad1c-d527e7ffbc94">
3. Click on save button and give it 10 - 20 secs, where supermemory extension will sync all your twitter bookmarks to supermemory.ai
4. Volia! Now your second brain has all your twitter bookmarks.
4. Voila! Now your second brain has all your twitter bookmarks.
## 👨‍💻 The Stack
@ -88,11 +88,11 @@ The database, auth etc logic is here
Built with:
- Nextjs 14
- [Nextjs 14](https://nextjs.org/)
- [Next Auth](https://next-auth.js.org/)
- [Drizzle ORM](https://drizzle.team/)
- [Cloudflare D1 database](https://developers.cloudflare.com/d1/get-started/)
- Cloudflare ratelimiter
- [Cloudflare ratelimiter](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/)
- [TailwindCSS](https://tailwindcss.com)
- [shadcn-ui](https://ui.shadcn.com)
- And some other amazing open source projects like [Novel](https://novel.sh) and [vaul](https://vaul.emilkowal.ski/)

View file

@ -1,6 +1,6 @@
# Self Hosting Guide
This guide will help you set up your own instance of Supermemory. This is neccessary if you want to contribute to the project or if you want to self host the project. You can read more about the stack [here](https://github.com/supermemoryai/supermemory/?tab=readme-ov-file#-the-stack).
This guide will help you set up your own instance of Supermemory. This is necessary if you want to contribute to the project or if you want to self host the project. You can read more about the stack [here](https://github.com/supermemoryai/supermemory/?tab=readme-ov-file#-the-stack).
## Prerequisites

View file

@ -13,9 +13,11 @@
"license": "MIT",
"dependencies": {
"@hono/zod-validator": "^0.2.1",
"hono": "^4.5.1"
"hono": "^4.5.1",
"honox": "^0.1.23",
"vite": "^5.3.5"
},
"devDependencies": {
"@cloudflare/workers-types": "^4.20240614.0"
}
"devDependencies": {
"@cloudflare/workers-types": "^4.20240614.0"
}
}

View file

@ -161,6 +161,7 @@ export async function batchCreateChunksAndEmbeddings({
vectors.push(...batchVectors);
}
console.log(
"vector Id list: ",
vectors.map((vector) => {
return vector.id;
}),
@ -185,6 +186,7 @@ export async function batchCreateChunksAndEmbeddings({
const results = [];
for (let i = 0; i < newVectors.length; i += 20) {
results.push(newVectors.slice(i, i + 20));
console.log(newVectors);
}
await Promise.all(

View file

@ -710,6 +710,47 @@ app.get(
},
);
app.get("/howFuckedAreWe", async (c) => {
let keys = 0;
const concurrencyLimit = 5; // Adjust this based on your system's capability
const queue: string[] = [undefined]; // Start with an undefined cursor
async function fetchKeys(cursor?: string): Promise<void> {
const response = await c.env.KV.list({ cursor });
keys += response.keys.length;
// @ts-ignore
if (response.cursor) {
// @ts-ignore
queue.push(response.cursor);
}
}
async function getAllKeys(): Promise<void> {
const promises: Promise<void>[] = [];
while (queue.length > 0) {
while (promises.length < concurrencyLimit && queue.length > 0) {
const cursor = queue.shift();
promises.push(fetchKeys(cursor));
}
await Promise.all(promises);
promises.length = 0; // Clear the promises array
}
}
await getAllKeys();
console.log(`Total number of keys: ${keys}`);
// on a scale of 200,000
// what % are we there?
const fuckedPercent = (keys / 200000) * 100;
return c.json({ fuckedPercent });
});
export default {
fetch: app.fetch,
queue,

View file

@ -22,16 +22,26 @@ export interface ThreadTweetData {
}
export function chunkThread(threadText: string): TweetChunks {
const thread = JSON.parse(threadText);
if (typeof thread == "string") {
console.log("DA WORKER FAILED DO SOMEHTING FIX DA WROKER");
let thread = threadText;
try {
thread = JSON.parse(threadText);
} catch (e) {
console.log("error: thread is not json.", e);
}
if (typeof threadText == "string") {
console.log("DA WORKER FAILED DO SOMEHTING FIX DA WROKER", thread);
const rawTweet = getRawTweet(thread);
console.log(rawTweet);
const parsedTweet: any = JSON.parse(rawTweet);
const chunkedTweet = chunkText(parsedTweet.text, 1536);
const metadata: Metadata = {
tweetId: parsedTweet.id_str,
tweetLinks: parsedTweet.entities.urls.map((url: any) => url.expanded_url),
tweetLinks: parsedTweet.entities?.urls.map(
(url: any) => url.expanded_url,
),
tweetVids:
parsedTweet.extended_entities?.media
.filter((media: any) => media.type === "video")
@ -46,8 +56,8 @@ export function chunkThread(threadText: string): TweetChunks {
return { type: "tweet", chunks };
} else {
console.log(JSON.stringify(thread));
const chunkedTweets = thread.map((tweet: Tweet) => {
console.log("thread in else statement", JSON.stringify(thread));
const chunkedTweets = (thread as any).map((tweet: Tweet) => {
const chunkedTweet = chunkText(tweet.text, 1536);
const metadata = {

View file

@ -131,6 +131,13 @@ export default function ContentApp({
});
}
});
const handleKeyDown = (e: KeyboardEvent) => {
if (isPopoverOpen) {
e.stopPropagation();
e.preventDefault();
}
};
document.addEventListener("keydown", handleKeyDown, true);
const portalDiv = document.createElement("div");
portalDiv.id = "popover-portal";
@ -139,6 +146,7 @@ export default function ContentApp({
return () => {
document.removeEventListener("mousemove", () => {});
document.removeEventListener("keydown", handleKeyDown, true);
};
}, []);

View file

@ -43,7 +43,7 @@ export function transformTweetData(input: any): Tweet | null {
display_text_range: tweet.legacy.display_text_range,
entities: {
hashtags: tweet.legacy.entities.hashtags,
urls: tweet.legacy.entities.urls,
urls: tweet.legacy.entities?.urls,
user_mentions: tweet.legacy.entities.user_mentions,
symbols: tweet.legacy.entities.symbols,
},

View file

@ -392,6 +392,24 @@ function Navbar() {
const router = useRouter();
const handleSkip = async () => {
await completeOnboarding();
toast.info("Creating memory...", {
icon: <PlusCircleIcon className="w-4 h-4 text-white animate-spin" />,
duration: 7500,
});
const cont = await createMemory({
content: "https://supermemory.ai",
spaces: undefined,
});
if (cont.success) {
toast.success("Memory created", {
richColors: true,
});
} else {
toast.error(`Memory creation failed: ${cont.error}`);
}
router.push("/home?q=what%20is%20supermemory");
};

View file

@ -15,10 +15,11 @@ async function Signin({
}: {
searchParams: Record<string, string>;
}) {
const telegramUser = searchParams.telegramUser;
const user = await auth();
if (user) {
redirect("/home");
redirect(`/home` + (telegramUser ? `?telegramUser=${telegramUser}` : ""));
}
return (

View file

@ -12,7 +12,7 @@ import {
} from "lucide-react";
import Image from "next/image";
import Link from "next/link";
import React, { useEffect, useMemo, useState } from "react";
import React, { useMemo, useState } from "react";
import Masonry from "react-layout-masonry";
import { getRawTweet } from "@repo/shared-types/utils";
import { MyTweet } from "../../../components/twitter/render-tweet";
@ -20,16 +20,19 @@ import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from "@repo/ui/shadcn/dropdown-menu";
import { Button } from "@repo/ui/shadcn/button";
import { addUserToSpace, deleteItem, moveItem } from "@/app/actions/doers";
import {
addUserToSpace,
deleteItem,
deleteSpace,
moveItem,
} from "@/app/actions/doers";
import { toast } from "sonner";
import { Input } from "@repo/ui/shadcn/input";
import { motion } from "framer-motion";
@ -59,6 +62,39 @@ export function MemoriesPage({
}, [tab]);
const [filter, setFilter] = useState(initialFilter);
const [spaces, setSpaces] = useState<StoredSpace[]>(memoriesAndSpaces.spaces);
// to delete a space
const handleDeleteSpace = async (id: number) => {
const response = await deleteSpace(id);
if (response?.success) {
setSpaces(spaces.filter((space) => space.id !== id));
toast.success("Space deleted");
} else {
toast.error("Failed to delete space");
}
};
const handleExport = () => {
const dataToExport = sortedItems.map((item) => ({
type: item.item,
date: new Date(item.date).toISOString(),
data: item.data,
}));
const json = JSON.stringify(dataToExport, null, 2);
const blob = new Blob([json], { type: "application/json" });
const url = URL.createObjectURL(blob);
const a = document.createElement("a");
a.href = url;
a.download = "memories_and_spaces.json";
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
};
// Sort Both memories and spaces by their savedAt and createdAt dates respectfully.
// The output should be just one single list of items
@ -71,7 +107,7 @@ export function MemoriesPage({
date: new Date(memory.savedAt), // Assuming savedAt is a string date
data: memory,
})),
...memoriesAndSpaces.spaces.map((space) => ({
...spaces.map((space) => ({
item: "space",
date: new Date(space.createdAt), // Assuming createdAt is a string date
data: space,
@ -103,7 +139,7 @@ export function MemoriesPage({
return false;
})
.sort((a, b) => b.date - a.date);
}, [memoriesAndSpaces.memories, memoriesAndSpaces.spaces, filter]);
}, [memoriesAndSpaces.memories, spaces, filter]);
return (
<div
@ -172,13 +208,21 @@ export function MemoriesPage({
</div>
)}
<Filters
setFilter={setFilter}
filter={filter}
filterMethods={
currentSpace ? SpaceFilterMethods : MemoriesFilterMethods
}
/>
<div className="flex justify-between w-full">
<Filters
setFilter={setFilter}
filter={filter}
filterMethods={
currentSpace ? SpaceFilterMethods : MemoriesFilterMethods
}
/>
<button
onClick={handleExport}
className={`transition px-6 py-2 rounded-xl hover:text-[#369DFD]" text-[#B3BCC5] bg-secondary hover:bg-secondary hover:text-[#76a3cc]`}
>
JSON Export
</button>
</div>
<Masonry
className="mt-6 relative"
@ -216,6 +260,7 @@ export function MemoriesPage({
title={(item.data as StoredSpace).name}
description={`${(item.data as StoredSpace).numItems} memories`}
id={(item.data as StoredSpace).id}
handleDeleteSpace={handleDeleteSpace}
/>
);
}
@ -231,34 +276,45 @@ function TabComponent({
title,
description,
id,
handleDeleteSpace,
}: {
title: string;
description: string;
id: number;
handleDeleteSpace: (id: number) => void;
}) {
return (
<Link
href={`/space/${id}`}
className="flex flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4"
>
<div className="flex group flex-col gap-4 bg-[#161f2a]/30 backdrop-blur-md border-2 border-border w-full rounded-xl p-4">
<div className="flex items-center gap-2 text-xs">
<Image alt="Spaces icon" src={MemoriesIcon} className="size-3" /> Space
</div>
<div className="flex items-center">
<div>
<div className="h-12 w-12 flex justify-center items-center rounded-md">
{title.slice(0, 2).toUpperCase()} {id}
<div>
<Link
href={`/space/${id}`}
className="flex items-center justify-between w-full"
>
<div>
<div className="h-12 w-12 flex justify-center items-center rounded-md">
{title.slice(0, 2).toUpperCase()} {id}
</div>
</div>
</div>
<div className="grow px-4">
<div className="text-lg text-[#fff] line-clamp-2">{title}</div>
<div>{description}</div>
</div>
<div>
<Image src={NextIcon} alt="Search icon" />
<div className="grow px-2">
<div className="text-lg text-[#fff] line-clamp-2">{title}</div>
<div>{description}</div>
</div>
<div>
<Image src={NextIcon} alt="Search icon" />
</div>
</Link>
<div className="absolute z-40 right-3 top-3 opacity-0 group-hover:opacity-100 hover:text-red-600">
<TrashIcon
onClick={() => handleDeleteSpace(id)}
className="w-4 cursor-pointer"
/>
</div>
</div>
</Link>
</div>
);
}

View file

@ -6,7 +6,7 @@ import QueryInput from "./chatQueryInput";
import { cn } from "@repo/ui/lib/utils";
import { motion } from "framer-motion";
import { useRouter } from "next/navigation";
import { ChatHistory, sourcesZod } from "@repo/shared-types";
import { type ChatHistory, sourcesZod } from "@repo/shared-types";
import {
Accordion,
AccordionContent,
@ -23,7 +23,11 @@ import { codeLanguageSubset } from "@/lib/constants";
import { toast } from "sonner";
import Link from "next/link";
import { createChatObject } from "@/app/actions/doers";
import { ClipboardIcon } from "@heroicons/react/24/outline";
import {
ClipboardIcon,
SpeakerWaveIcon,
SpeakerXMarkIcon,
} from "@heroicons/react/24/outline";
function ChatWindow({
q,
@ -51,6 +55,8 @@ function ChatWindow({
}) {
const [layout, setLayout] = useState<"chat" | "initial">("chat");
const [chatHistory, setChatHistory] = useState<ChatHistory[]>(initialChat);
const [speakingIdx, setSpeakingIdx] = useState<number | null>(null);
const speechSynth: SpeechSynthesis = window.speechSynthesis;
const removeJustificationFromText = (text: string) => {
// remove everything after the first "<justification>" word
@ -66,12 +72,31 @@ function ChatWindow({
return text;
};
const handleTTS = (text: string, idx: number) => {
if (speakingIdx != null) return stopTTS();
if (!text) return;
const utterThis: SpeechSynthesisUtterance = new SpeechSynthesisUtterance(
text,
);
utterThis.lang = "en-US";
speechSynth.speak(utterThis);
setSpeakingIdx(idx);
utterThis.onend = () => {
setSpeakingIdx(null);
};
};
const stopTTS = () => {
speechSynth.cancel();
setSpeakingIdx(null);
};
const router = useRouter();
const getAnswer = async (
query: string,
spaces: string[],
proMode: boolean = false,
proMode = false,
) => {
if (query.trim() === "from_loading" || query.trim().length === 0) {
return;
@ -263,10 +288,10 @@ function ChatWindow({
</div>
)}
<div className="flex flex-col mt-2">
<div>
<div className="flex flex-col mt-2 w-full">
<div className="w-full">
<div className="text-foreground-menu py-2">Answer</div>
<div className="text-base">
<div className="text-base prose prose-invert prose-headings:py-0 prose-h1:py-4 prose-h2:py-4 prose-headings:-my-2 prose-h1:text-2xl prose-h2:text-xl prose:min-w-full min-w-full">
{/* Loading state */}
{(chat.answer.parts.length === 0 ||
chat.answer.parts.join("").length === 0) && (
@ -291,11 +316,7 @@ function ChatWindow({
},
],
]}
components={{
code: code as any,
p: p as any,
}}
className="flex flex-col gap-2 text-base"
className="flex flex-col gap-2 w-full"
>
{removeJustificationFromText(
chat.answer.parts
@ -305,10 +326,6 @@ function ChatWindow({
</Markdown>
<div className="mt-3 relative -left-2 flex items-center gap-1">
{/* TODO: speak response */}
{/* <button className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200">
<SpeakerWaveIcon className="size-[18px] group-hover:text-primary" />
</button> */}
{/* copy response */}
<button
onClick={() =>
@ -322,6 +339,27 @@ function ChatWindow({
>
<ClipboardIcon className="size-[18px] group-hover:text-primary" />
</button>
{/* speak response */}
<button
disabled={
speakingIdx !== null && speakingIdx !== idx
}
onClick={() => {
handleTTS(
chat.answer.parts
.map((part) => part.text)
.join(""),
idx,
);
}}
className="group h-8 w-8 flex justify-center items-center active:scale-75 duration-200"
>
{speakingIdx === idx ? (
<SpeakerXMarkIcon className="size-[18px] group-hover:text-primary" />
) : (
<SpeakerWaveIcon className="size-[18px] group-hover:text-primary group-disabled:text-gray-600" />
)}
</button>
</div>
</div>
</div>
@ -342,10 +380,10 @@ function ChatWindow({
</AccordionTrigger>
{/* TODO: fade out content on the right side, the fade goes away when the user scrolls */}
<AccordionContent
className="flex flex-col no-scrollbar overflow-auto gap-4 relative max-w-3xl no-scrollbar"
className="flex flex-col gap-4 relative max-w-3xl overflow-x-auto scrollbar-thin scrollbar-thumb-scrollbar-thumb scrollbar-track-scrollbar-track scrollbar-thumb-rounded"
defaultChecked
>
<div className="w-full no-scrollbar flex gap-4">
<div className="w-full flex gap-3">
{/* Loading state */}
{chat.answer.sources.length > 0 ||
(chat.answer.parts.length === 0 && (
@ -373,7 +411,9 @@ function ChatWindow({
<span>{source.type}</span>
{source.numChunks > 1 && (
<span>{source.numChunks} chunks</span>
<span className="font-bold">
{source.numChunks} chunks
</span>
)}
</div>
<div className="text-base">
@ -388,40 +428,36 @@ function ChatWindow({
))}
</div>
{chat.answer.justification &&
chat.answer.justification.length && (
<div
className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
{chat.answer.justification?.length && (
<div
className={`${chat.answer.justification && chat.answer.justification.length > 0 ? "flex" : "hidden"}`}
>
<Accordion
defaultValue={""}
type="single"
collapsible
>
<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>
)}
<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>
)}
</AccordionContent>
</AccordionItem>
</Accordion>

View file

@ -78,7 +78,7 @@ function Page({ searchParams }: { searchParams: Record<string, string> }) {
}}
>
<motion.h1
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-1"
animate={{
opacity: query.length ? 0 : 1,
translateY: query.length ? "10px" : "0px",

View file

@ -74,7 +74,7 @@ function QueryInput({
initialSpaces={initialSpaces || []}
/>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2 p-2 rounded-lg bg-[#369DFD1A]">
{/* <div className="flex items-center gap-2 p-2 rounded-lg bg-[#369DFD1A]">
<Label htmlFor="pro-mode" className="text-sm">
Pro mode
</Label>
@ -84,7 +84,7 @@ function QueryInput({
id="pro-mode"
about="Pro mode"
/>
</div>
</div> */}
<button type="submit" className="rounded-lg bg-[#369DFD1A] p-3">
<Image src={ArrowRightIcon} alt="Enter" />
</button>

View file

@ -30,6 +30,7 @@ import { createMemory, createSpace } from "../actions/doers";
import ComboboxWithCreate from "@repo/ui/shadcn/combobox";
import { StoredSpace } from "@repo/db/schema";
import useMeasure from "react-use-measure";
import { useKeyPress } from "@/lib/useKeyPress";
function Menu() {
const [spaces, setSpaces] = useState<StoredSpace[]>([]);
@ -48,7 +49,11 @@ function Menu() {
setSpaces(spaces.data);
})();
}, []);
useKeyPress("a", () => {
if (!dialogOpen) {
setDialogOpen(true);
}
});
const menuItems = [
{
icon: HomeIconWeb,
@ -106,33 +111,23 @@ function Menu() {
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;
throw new Error("Content is required");
}
console.log(spaces);
const cont = await createMemory({
content: content,
spaces: spaces ?? undefined,
});
setContent("");
setSelectedSpaces([]);
if (cont.success) {
toast.success("Memory queued", {
richColors: true,
});
} else {
toast.error(`Memory creation failed: ${cont.error}`);
throw new Error(`Memory creation failed: ${cont.error}`);
return cont;
}
};
@ -188,8 +183,17 @@ function Menu() {
<form
action={async (e: FormData) => {
const content = e.get("content")?.toString();
await handleSubmit(content, selectedSpaces);
toast.promise(handleSubmit(content, selectedSpaces), {
loading: (
<span>
<PlusCircleIcon className="w-4 h-4 inline mr-2 text-white animate-spin" />{" "}
Creating memory...
</span>
),
success: (data) => "Memory created",
error: (error) => error.message,
richColors: true,
});
}}
className="flex flex-col gap-4 "
>
@ -213,7 +217,17 @@ function Menu() {
onKeyDown={(e) => {
if (e.key === "Enter" && !e.shiftKey) {
e.preventDefault();
handleSubmit(content, selectedSpaces);
toast.promise(handleSubmit(content, selectedSpaces), {
loading: (
<span>
<PlusCircleIcon className="w-4 h-4 inline mr-2 text-white animate-spin" />{" "}
Creating memory...
</span>
),
success: (data) => "Memory created",
error: (error) => error.message,
richColors: true,
});
}
}}
/>
@ -266,10 +280,7 @@ function Menu() {
]);
setSelectedSpaces((prev) => [...prev, creationTask.data!]);
} else {
toast.error(
"Space creation failed: " + creationTask.error ??
"Unknown error",
);
toast.error("Space creation failed: " + creationTask.error);
}
}}
placeholder="Select or create a new space."

View file

@ -39,7 +39,7 @@ const ImageComponent = memo(({ id }: { id: string }) => {
return (
<div className="w-full aspect-video bg-[#2C3439] flex justify-center items-center">
Drew things to seee here
Draw things. They will appear here.
</div>
);
});

View file

@ -21,7 +21,7 @@ async function page() {
<BlurHeaderMenu />
<div className="w-full flex py-20">
<div className="w-full flex py-20 justify-center">
{!canvas.success || canvas.error ? (
<div>Hmmm... Something went wrong. :/</div>
) : (
@ -37,7 +37,7 @@ async function page() {
</div>
<h3 className="fixed left-1/2 -translate-x-1/2 bottom-4 text-gray-400 pt-20 text-center">
*this is under beta and only one canvas is allowed per user
Thinkpads is under beta and only one thinkpad is allowed per user.
</h3>
</div>
);

View file

@ -167,6 +167,17 @@ const getTweetData = async (tweetID: string) => {
return data;
};
export const deleteSpace = async (id: number) => {
try {
await db.delete(space).where(eq(space.id, id));
return {
success: true,
};
} catch (e) {
console.log(e);
}
};
export const createMemory = async (input: {
content: string;
spaces?: number[];

View file

@ -24,8 +24,8 @@ export const createMemoryFromAPI = async (input: {
method: "POST",
body: JSON.stringify({
pageContent: input.data.pageContent,
title: input.data.title,
description: input.data.description,
title: input.data.title.slice(0, 500),
description: input.data.description.slice(0, 500),
url: input.data.url,
spaces: input.data.spaces,
user: input.userId,

View file

@ -2,6 +2,7 @@ import { type NextRequest } from "next/server";
import { addFromAPIType } from "@repo/shared-types";
import { ensureAuth } from "../ensureAuth";
import { createMemoryFromAPI } from "./helper";
import { getRawTweet } from "@repo/shared-types/utils";
export const runtime = "edge";
@ -16,7 +17,43 @@ export async function POST(req: NextRequest) {
return new Response("Missing BACKEND_SECURITY_KEY", { status: 500 });
}
const body = await req.json();
let body;
try {
body = await req.json();
} catch (e) {
const error = (e as Error).message;
console.log(error);
const tryJson = getRawTweet(await req.text());
console.log(tryJson);
if (tryJson) {
try {
body = JSON.parse(tryJson);
} catch (e) {
console.log(e);
return new Response(
JSON.stringify({
message: "Raw found but not json?" + error,
}),
{
status: 400,
},
);
}
} else {
return new Response(
JSON.stringify({
message: "Raw not found & not json." + error,
}),
{
status: 400,
},
);
}
}
const validated = addFromAPIType.safeParse(body);

View file

@ -68,9 +68,9 @@ bot.on("message", async (ctx) => {
if (response.status !== 200) {
console.log("Failed to get response from backend");
console.log(response.status);
console.log(await response.text());
await ctx.reply(
"Sorry, I am not able to process your request at the moment.",
"Sorry, I am not able to process your request at the moment." +
(await response.text()),
);
return;
}

View file

@ -1,118 +0,0 @@
import { Button } from "@repo/ui/shadcn/button";
import { auth, signIn, signOut } from "../../server/auth";
import { db } from "../../server/db";
import { sql } from "drizzle-orm";
import { users } from "@repo/db/schema";
import { getThemeToggler } from "../../lib/get-theme-button";
export const runtime = "edge";
export default async function Page() {
const usr = await auth();
const userCount = await db
.select({
count: sql<number>`count(*)`.mapWith(Number),
})
.from(users);
const SetThemeButton = getThemeToggler();
return (
<main className="flex flex-col items-center justify-center min-h-screen">
<div className="flex max-w-2xl justify-between w-full">
<SetThemeButton />
<div className="flex gap-2 items-center justify-center">
{" "}
<svg
viewBox="0 0 256 116"
xmlns="http://www.w3.org/2000/svg"
width="45px"
height="45px"
preserveAspectRatio="xMidYMid"
>
<path
fill="#FFF"
d="m202.357 49.394-5.311-2.124C172.085 103.434 72.786 69.289 66.81 85.997c-.996 11.286 54.227 2.146 93.706 4.059 12.039.583 18.076 9.671 12.964 24.484l10.069.031c11.615-36.209 48.683-17.73 50.232-29.68-2.545-7.857-42.601 0-31.425-35.497Z"
/>
<path
fill="#F4811F"
d="M176.332 108.348c1.593-5.31 1.062-10.622-1.593-13.809-2.656-3.187-6.374-5.31-11.154-5.842L71.17 87.634c-.531 0-1.062-.53-1.593-.53-.531-.532-.531-1.063 0-1.594.531-1.062 1.062-1.594 2.124-1.594l92.946-1.062c11.154-.53 22.839-9.56 27.087-20.182l5.312-13.809c0-.532.531-1.063 0-1.594C191.203 20.182 166.772 0 138.091 0 111.535 0 88.697 16.995 80.73 40.896c-5.311-3.718-11.684-5.843-19.12-5.31-12.747 1.061-22.838 11.683-24.432 24.43-.531 3.187 0 6.374.532 9.56C16.996 70.107 0 87.103 0 108.348c0 2.124 0 3.718.531 5.842 0 1.063 1.062 1.594 1.594 1.594h170.489c1.062 0 2.125-.53 2.125-1.594l1.593-5.842Z"
/>
<path
fill="#FAAD3F"
d="M205.544 48.863h-2.656c-.531 0-1.062.53-1.593 1.062l-3.718 12.747c-1.593 5.31-1.062 10.623 1.594 13.809 2.655 3.187 6.373 5.31 11.153 5.843l19.652 1.062c.53 0 1.062.53 1.593.53.53.532.53 1.063 0 1.594-.531 1.063-1.062 1.594-2.125 1.594l-20.182 1.062c-11.154.53-22.838 9.56-27.087 20.182l-1.063 4.78c-.531.532 0 1.594 1.063 1.594h70.108c1.062 0 1.593-.531 1.593-1.593 1.062-4.25 2.124-9.03 2.124-13.81 0-27.618-22.838-50.456-50.456-50.456"
/>
</svg>
<span className="italic">Cloudflare Next Saas Starter</span>
</div>
<div className="border border-black dark:border-white rounded-2xl p-2 flex items-center">
Start by editing apps/web/page.tsx
</div>
</div>
<div className="max-w-2xl text-start w-full mt-16">
Welcome to Cloudflare Next Saas Starter. <br /> Built a full stack app
using production-ready tools and frameworks, host on Cloudflare
instantly.
<br />
An opinionated, batteries-included framework with{" "}
<a
className="text-transparent bg-clip-text bg-gradient-to-r from-[#a93d64] to-[#275ba9]"
href="https://turbo.build"
>
Turborepo
</a>{" "}
and Nextjs. Fully Typesafe. Best practices followed by default.
<br /> <br />
Here's what the stack includes:
<ul className="list-disc mt-4 prose dark:prose-invert">
<li>
Authentication with <code>next-auth</code>
</li>
<li>Database using Cloudflare's D1 serverless databases</li>
<li>Drizzle ORM, already connected to your database and auth </li>
<li>Light/darkmode theming that works with server components (!)</li>
<li>Styling using TailwindCSS and ShadcnUI</li>
<li>Turborepo with a landing page and shared components</li>
<li>Cloudflare wrangler for quick functions on the edge</li>
<li>
... best part: everything's already set up for you. Just code!
</li>
</ul>
<div className="mt-4 flex flex-col gap-2">
<span>Number of users in database: {userCount[0]!.count}</span>
</div>
{usr?.user?.email ? (
<>
<div className="mt-4 flex flex-col gap-2">
<span>Hello {usr.user.name} 👋</span>
<span>{usr.user.email}</span>
</div>
<form
action={async () => {
"use server";
await signOut();
}}
>
<Button variant={"destructive"} className="mt-4">
Sign out
</Button>
</form>
</>
) : (
<form
action={async () => {
"use server";
await signIn("google");
}}
>
<Button className="mt-4">Login with Google</Button>
</form>
)}
</div>
</main>
);
}

View file

@ -0,0 +1,15 @@
import { useEffect } from "react";
export const useKeyPress = (key: string, callback: () => void) => {
useEffect(() => {
const handler = (e: KeyboardEvent) => {
if (e.key === key && e.altKey) {
callback();
}
};
window.addEventListener("keydown", handler);
return () => {
window.removeEventListener("keydown", handler);
};
}, [key, callback]);
};

View file

@ -21,6 +21,7 @@
"@radix-ui/react-popover": "^1.0.7",
"@radix-ui/react-slot": "^1.1.0",
"@sentry/nextjs": "^8",
"ai": "^3.3.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
"drizzle-orm": "0.30.0",

View file

@ -12,14 +12,6 @@ export const {
} = NextAuth({
secret: process.env.BACKEND_SECURITY_KEY,
trustHost: true,
// callbacks: {
// session: ({ session, token, user }) => ({
// ...session,
// user: {
// ...session.user,
// },
// }),
// },
adapter: DrizzleAdapter(db, {
usersTable: users,
accountsTable: accounts,

View file

@ -1 +1,20 @@
module.exports = require("@repo/tailwind-config/tailwind.config");
// Import the existing Tailwind config from your shared repository
const sharedConfig = require("@repo/tailwind-config/tailwind.config");
module.exports = {
presets: [sharedConfig],
theme: {
extend: {
colors: {
scrollbar: {
// thumb: "#d1d5db",
// thumbHover: "#1D4ED8",
thumb: "#303c4c",
thumbHover: "#2E3A48",
track: "#1F2937",
},
},
},
},
plugins: [require("tailwind-scrollbar")({ nocompatible: true })],
};

View file

@ -1,6 +1,7 @@
"use client";
import * as React from "react";
import React, { useRef, useEffect, useState } from "react";
import * as AccordionPrimitive from "@radix-ui/react-accordion";
import { ChevronDownIcon } from "@heroicons/react/24/outline";
@ -40,15 +41,72 @@ AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName;
const AccordionContent = React.forwardRef<
React.ElementRef<typeof AccordionPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof AccordionPrimitive.Content>
>(({ className, children, ...props }, ref) => (
<AccordionPrimitive.Content
ref={ref}
className="overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div className={cn("pb-4 pt-0", className)}>{children}</div>
</AccordionPrimitive.Content>
));
>(({ className, children, ...props }, ref) => {
const containerRef = useRef<HTMLDivElement>(null);
const [fadeWidth, setFadeWidth] = useState(0);
useEffect(() => {
const handleWheel = (event: WheelEvent) => {
if (containerRef.current) {
event.preventDefault();
if (Math.abs(event.deltaX) > Math.abs(event.deltaY)) {
containerRef.current.scrollLeft += event.deltaX;
} else {
containerRef.current.scrollLeft += event.deltaY;
}
}
};
const handleScroll = () => {
if (containerRef.current) {
const { scrollWidth, clientWidth, scrollLeft } = containerRef.current;
const calculatedFadeWidth = Math.min(
8,
scrollWidth - clientWidth - scrollLeft,
);
setFadeWidth(calculatedFadeWidth);
}
};
const currentRef = containerRef.current;
currentRef?.addEventListener("wheel", handleWheel);
currentRef?.addEventListener("scroll", handleScroll);
handleScroll();
return () => {
currentRef?.removeEventListener("wheel", handleWheel);
currentRef?.removeEventListener("scroll", handleScroll);
};
}, []);
const fadeStyle: React.CSSProperties = {
position: "absolute",
top: 0,
right: 0,
width: `${fadeWidth}px`,
height: "85%",
pointerEvents: "none",
background: "linear-gradient(to left, rgb(46, 58, 72), transparent)",
};
return (
<AccordionPrimitive.Content
ref={ref}
className="relative overflow-hidden text-sm transition-all data-[state=closed]:animate-accordion-up data-[state=open]:animate-accordion-down"
{...props}
>
<div
ref={containerRef}
className={cn("pb-4 pt-0", className)}
style={{ position: "relative" }}
>
{children}
</div>
{/* Fade-out effect with inline styles */}
<div style={fadeStyle}></div>
</AccordionPrimitive.Content>
);
});
AccordionContent.displayName = AccordionPrimitive.Content.displayName;

877
pnpm-lock.yaml generated

File diff suppressed because it is too large Load diff