diff options
| author | Dhravya Shah <[email protected]> | 2024-08-06 11:05:02 -0700 |
|---|---|---|
| committer | Dhravya Shah <[email protected]> | 2024-08-06 11:05:02 -0700 |
| commit | be3e13b4bfb847af410f2159be462cc912141fc3 (patch) | |
| tree | a3783a7e7798de60f599ea0f0a5db24c5c30395f /apps | |
| parent | changes for prod (diff) | |
| parent | Merge pull request #219 from Deepakchowdavarapu/readme-issue (diff) | |
| download | supermemory-be3e13b4bfb847af410f2159be462cc912141fc3.tar.xz supermemory-be3e13b4bfb847af410f2159be462cc912141fc3.zip | |
merged latest changes with queue branch and ready for prod
Diffstat (limited to 'apps')
24 files changed, 393 insertions, 251 deletions
diff --git a/apps/cf-ai-backend/package.json b/apps/cf-ai-backend/package.json index 2b83cc93..3fcf71e0 100644 --- a/apps/cf-ai-backend/package.json +++ b/apps/cf-ai-backend/package.json @@ -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" + } } diff --git a/apps/cf-ai-backend/src/helper.ts b/apps/cf-ai-backend/src/helper.ts index c4dc8de1..70efaecd 100644 --- a/apps/cf-ai-backend/src/helper.ts +++ b/apps/cf-ai-backend/src/helper.ts @@ -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( diff --git a/apps/cf-ai-backend/src/index.ts b/apps/cf-ai-backend/src/index.ts index a46d0080..1f391359 100644 --- a/apps/cf-ai-backend/src/index.ts +++ b/apps/cf-ai-backend/src/index.ts @@ -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, diff --git a/apps/cf-ai-backend/src/queueConsumer/chunkers/chunkTweet.ts b/apps/cf-ai-backend/src/queueConsumer/chunkers/chunkTweet.ts index f4dd2e16..46a56410 100644 --- a/apps/cf-ai-backend/src/queueConsumer/chunkers/chunkTweet.ts +++ b/apps/cf-ai-backend/src/queueConsumer/chunkers/chunkTweet.ts @@ -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 = { diff --git a/apps/extension/content/ContentApp.tsx b/apps/extension/content/ContentApp.tsx index c0339897..a510a77c 100644 --- a/apps/extension/content/ContentApp.tsx +++ b/apps/extension/content/ContentApp.tsx @@ -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); }; }, []); diff --git a/apps/extension/helpers.ts b/apps/extension/helpers.ts index 9e95f963..029de5c7 100644 --- a/apps/extension/helpers.ts +++ b/apps/extension/helpers.ts @@ -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, }, diff --git a/apps/web/app/(auth)/onboarding/page.tsx b/apps/web/app/(auth)/onboarding/page.tsx index c311ea13..9a6ac481 100644 --- a/apps/web/app/(auth)/onboarding/page.tsx +++ b/apps/web/app/(auth)/onboarding/page.tsx @@ -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"); }; diff --git a/apps/web/app/(auth)/signin/page.tsx b/apps/web/app/(auth)/signin/page.tsx index 3b563b90..35e6bab6 100644 --- a/apps/web/app/(auth)/signin/page.tsx +++ b/apps/web/app/(auth)/signin/page.tsx @@ -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 ( diff --git a/apps/web/app/(dash)/(memories)/content.tsx b/apps/web/app/(dash)/(memories)/content.tsx index 431109be..6e2659cb 100644 --- a/apps/web/app/(dash)/(memories)/content.tsx +++ b/apps/web/app/(dash)/(memories)/content.tsx @@ -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> ); } diff --git a/apps/web/app/(dash)/chat/chatWindow.tsx b/apps/web/app/(dash)/chat/chatWindow.tsx index 3d7ca295..60acd16d 100644 --- a/apps/web/app/(dash)/chat/chatWindow.tsx +++ b/apps/web/app/(dash)/chat/chatWindow.tsx @@ -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> diff --git a/apps/web/app/(dash)/home/page.tsx b/apps/web/app/(dash)/home/page.tsx index 0545d1ff..18bd934d 100644 --- a/apps/web/app/(dash)/home/page.tsx +++ b/apps/web/app/(dash)/home/page.tsx @@ -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", diff --git a/apps/web/app/(dash)/home/queryinput.tsx b/apps/web/app/(dash)/home/queryinput.tsx index 995fea53..e7f400da 100644 --- a/apps/web/app/(dash)/home/queryinput.tsx +++ b/apps/web/app/(dash)/home/queryinput.tsx @@ -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> diff --git a/apps/web/app/(dash)/menu.tsx b/apps/web/app/(dash)/menu.tsx index 783a4780..1c0ce1ee 100644 --- a/apps/web/app/(dash)/menu.tsx +++ b/apps/web/app/(dash)/menu.tsx @@ -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." diff --git a/apps/web/app/(thinkpad)/thinkpad/image.tsx b/apps/web/app/(thinkpad)/thinkpad/image.tsx index b55b9a81..7f3de2ff 100644 --- a/apps/web/app/(thinkpad)/thinkpad/image.tsx +++ b/apps/web/app/(thinkpad)/thinkpad/image.tsx @@ -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> ); }); diff --git a/apps/web/app/(thinkpad)/thinkpad/page.tsx b/apps/web/app/(thinkpad)/thinkpad/page.tsx index defa8e43..ab2bf88e 100644 --- a/apps/web/app/(thinkpad)/thinkpad/page.tsx +++ b/apps/web/app/(thinkpad)/thinkpad/page.tsx @@ -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> ); diff --git a/apps/web/app/actions/doers.ts b/apps/web/app/actions/doers.ts index 500a8608..c11d5f0a 100644 --- a/apps/web/app/actions/doers.ts +++ b/apps/web/app/actions/doers.ts @@ -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[]; diff --git a/apps/web/app/api/store/helper.ts b/apps/web/app/api/store/helper.ts index 6ab7fb23..db13ca91 100644 --- a/apps/web/app/api/store/helper.ts +++ b/apps/web/app/api/store/helper.ts @@ -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, diff --git a/apps/web/app/api/store/route.ts b/apps/web/app/api/store/route.ts index 12af7894..ad81c7c4 100644 --- a/apps/web/app/api/store/route.ts +++ b/apps/web/app/api/store/route.ts @@ -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); diff --git a/apps/web/app/api/telegram/route.ts b/apps/web/app/api/telegram/route.ts index dddfa2f4..c629e409 100644 --- a/apps/web/app/api/telegram/route.ts +++ b/apps/web/app/api/telegram/route.ts @@ -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; } diff --git a/apps/web/app/ref/page.tsx b/apps/web/app/ref/page.tsx deleted file mode 100644 index c582fe5c..00000000 --- a/apps/web/app/ref/page.tsx +++ /dev/null @@ -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> - ); -} diff --git a/apps/web/lib/useKeyPress.ts b/apps/web/lib/useKeyPress.ts new file mode 100644 index 00000000..eee23acb --- /dev/null +++ b/apps/web/lib/useKeyPress.ts @@ -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]); +}; diff --git a/apps/web/package.json b/apps/web/package.json index a5332157..d3bf1f48 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -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", diff --git a/apps/web/server/auth.ts b/apps/web/server/auth.ts index 5d8b15d1..645989fa 100644 --- a/apps/web/server/auth.ts +++ b/apps/web/server/auth.ts @@ -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, diff --git a/apps/web/tailwind.config.ts b/apps/web/tailwind.config.ts index cf1434cf..2a05cd74 100644 --- a/apps/web/tailwind.config.ts +++ b/apps/web/tailwind.config.ts @@ -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 })], +}; |