merge conflicts

This commit is contained in:
Dhravya 2024-06-30 20:50:24 -05:00
commit ffd141ade4
34 changed files with 1012 additions and 256 deletions

View file

@ -1,129 +1,17 @@
"use client";
import { Canvas } from "@repo/ui/components/canvas/components/canvas";
import React, { useState } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { SettingsIcon, DragIcon } from "@repo/ui/icons";
import DraggableComponentsContainer from "@repo/ui/components/canvas/components/draggableComponent";
import { AutocompleteIcon, blockIcon } from "@repo/ui/icons";
import Image from "next/image";
import { Switch } from "@repo/ui/shadcn/switch";
import { Label } from "@repo/ui/shadcn/label";
import { useRouter } from "next/router";
function page() {
const [fullScreen, setFullScreen] = useState(false);
const [visible, setVisible] = useState(true);
const router = useRouter();
router.push("/home");
import { userHasCanvas } from "@/app/actions/fetchers";
import {
RectProvider,
ResizaleLayout,
} from "@/components/canvas/resizableLayout";
import { redirect } from "next/navigation";
export default async function page({ params }: any) {
const canvasExists = await userHasCanvas(params.id);
if (!canvasExists.success) {
redirect("/canvas");
}
return (
<div
className={`h-screen w-full ${!fullScreen ? "px-4 py-6" : "bg-[#1F2428]"} transition-all`}
>
<div>
<PanelGroup
onLayout={(l) => {
l[0]! < 20 ? setVisible(false) : setVisible(true);
}}
className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`}
direction="horizontal"
>
<Panel
onExpand={() => {
setTimeout(() => setFullScreen(false), 50);
}}
onCollapse={() => {
setTimeout(() => setFullScreen(true), 50);
}}
defaultSize={30}
collapsible={true}
>
<div
className={`flex transition-all rounded-2xl ${fullScreen ? "h-screen" : "h-[calc(100vh-3rem)]"} w-full flex-col overflow-hidden bg-[#1F2428]`}
>
<div className="flex items-center justify-between bg-[#2C3439] px-4 py-2 text-lg font-medium text-[#989EA4]">
Change Filters
<Image src={SettingsIcon} alt="setting-icon" />
</div>
{visible ? (
<SidePanel />
) : (
<h1 className="text-center py-10 text-xl">
Need more space to show!
</h1>
)}
</div>
</Panel>
<PanelResizeHandle
className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`}
>
<div
className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`}
>
<Image src={DragIcon} alt="drag-icon" />
</div>
</PanelResizeHandle>
<Panel className="relative" defaultSize={70} minSize={60}>
<div
className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`}
>
<Canvas />
</div>
</Panel>
</PanelGroup>
</div>
</div>
<RectProvider id={params.id}>
<ResizaleLayout />
</RectProvider>
);
}
function SidePanel() {
const [value, setValue] = useState("");
const [dragAsText, setDragAsText] = useState(false);
return (
<>
<div className="px-3 py-5">
<input
placeholder="search..."
onChange={(e) => {
setValue(e.target.value);
}}
value={value}
// rows={1}
className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52"
/>
</div>
<div className="flex items-center justify-end px-3 py-4">
<Switch
className="bg-[#151515] data-[state=unchecked]:bg-red-400 data-[state=checked]:bg-blue-400"
onCheckedChange={(e) => setDragAsText(e)}
id="drag-text-mode"
/>
<Label htmlFor="drag-text-mode">Drag as Text</Label>
</div>
<DraggableComponentsContainer content={content} />
</>
);
}
export default page;
const content = [
{
content:
"Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.",
icon: AutocompleteIcon,
iconAlt: "Autocomplete",
extraInfo:
"Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a",
},
{
content:
"As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)",
icon: blockIcon,
iconAlt: "Autocomplete",
extraInfo:
"Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia",
},
];

View file

@ -1,13 +0,0 @@
import "../canvasStyles.css";
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<div lang="en" className="bg-[#151515]">
<div>{children}</div>
</div>
);
}

View file

@ -1,9 +1,22 @@
import { redirect } from "next/navigation";
import React from "react";
import { getCanvas } from "@/app/actions/fetchers";
import SearchandCreate from "./search&create";
import ThinkPads from "./thinkPads";
function page() {
redirect("/signin");
return <div>page</div>;
async function page() {
const canvas = await getCanvas();
return (
<div className="h-screen w-full py-32 text-[#FFFFFF] ">
<div className="flex w-full flex-col items-center gap-8">
<h1 className="text-4xl font-medium">Your thinkpads</h1>
<SearchandCreate />
{
// @ts-ignore
canvas.success && <ThinkPads data={canvas.data} />
}
</div>
</div>
);
}
export default page;

View file

@ -0,0 +1,45 @@
"use client";
import { useFormStatus } from "react-dom";
import Image from "next/image";
import { SearchIcon } from "@repo/ui/icons";
import { createCanvas } from "@/app/actions/doers";
import { toast } from "sonner";
export default function SearchandCreate() {
return (
<div className="flex w-[90%] max-w-2xl gap-2">
<div className="flex flex-grow items-center overflow-hidden rounded-xl bg-[#1F2428]">
<input
placeholder="search here..."
className="flex-grow bg-[#1F2428] px-5 py-3 text-xl focus:border-none focus:outline-none"
/>
<button className="h-full border-l-2 border-[#384149] px-2 pl-2">
<Image src={SearchIcon} alt="search" />
</button>
</div>
<form
action={async () => {
const res = await createCanvas();
if (!res.success) {
toast.warning(res.message, {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
}
}}
>
<Button />
</form>
</div>
);
}
function Button() {
const { pending } = useFormStatus();
return (
<button className="rounded-xl bg-[#1F2428] px-5 py-3 text-xl text-[#B8C4C6]">
{pending ? "Creating.." : "Create New"}
</button>
);
}

View file

@ -0,0 +1,276 @@
import { getCanvasData } from "@/app/actions/fetchers";
import { AnimatePresence, motion } from "framer-motion";
import Link from "next/link";
import {
EllipsisHorizontalCircleIcon,
TrashIcon,
PencilSquareIcon,
} from "@heroicons/react/24/outline";
import { toast } from "sonner";
import { Label } from "@repo/ui/shadcn/label";
const childVariants = {
hidden: { opacity: 0, y: 10, filter: "blur(2px)" },
visible: { opacity: 1, y: 0, filter: "blur(0px)" },
};
export default function ThinkPad({
title,
description,
image,
id,
}: {
title: string;
description: string;
image: string;
id: string;
}) {
const [deleted, setDeleted] = useState(false);
const [info, setInfo] = useState({ title, description });
return (
<AnimatePresence mode="sync">
{!deleted && (
<motion.div
layout
exit={{ opacity: 0, scaleY: 0 }}
variants={childVariants}
className="flex h-48 origin-top relative gap-4 rounded-2xl bg-[#1F2428] p-2"
>
<Link
className="h-full select-none min-w-[40%] bg-[#363f46] rounded-xl overflow-hidden"
href={`/canvas/${id}`}
>
<Suspense
fallback={
<div className=" h-full w-full flex justify-center items-center">
Loading...
</div>
}
>
<ImageComponent id={id} />
</Suspense>
</Link>
<div className="flex flex-col gap-2">
<motion.h2
initial={{ opacity: 0, filter: "blur(3px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
key={info.title}
>
{info.title}
</motion.h2>
<motion.h3
key={info.description}
initial={{ opacity: 0, filter: "blur(3px)" }}
animate={{ opacity: 1, filter: "blur(0px)" }}
className="overflow-hidden text-ellipsis text-[#B8C4C6]"
>
{info.description}
</motion.h3>
</div>
<Menu
info={info}
id={id}
setDeleted={() => setDeleted(true)}
setInfo={(e) => setInfo(e)}
/>
</motion.div>
)}
</AnimatePresence>
);
}
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@repo/ui/shadcn/popover";
function Menu({
info,
id,
setDeleted,
setInfo,
}: {
info: { title: string; description: string };
id: string;
setDeleted: () => void;
setInfo: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
}) {
return (
<Popover>
<PopoverTrigger className="absolute z-20 top-0 right-0" asChild>
<Button variant="secondary">
<EllipsisHorizontalCircleIcon className="size-5 stroke-2 stroke-[#B8C4C6]" />
</Button>
</PopoverTrigger>
<PopoverContent
align="start"
className="w-32 px-2 py-2 bg-[#161f2a]/30 text-[#B8C4C6] border-border flex flex-col gap-3"
>
<EditToolbar info={info} id={id} setInfo={setInfo} />
<Button
onClick={async () => {
const res = await deleteCanvas(id);
if (res.success) {
toast.success("Thinkpad removed.", {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
setDeleted();
} else {
toast.warning("Something went wrong.", {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
}
}}
className="flex gap-2 border-border"
variant="outline"
>
<TrashIcon className="size-8 stroke-1" /> Delete
</Button>
</PopoverContent>
</Popover>
);
}
function EditToolbar({
id,
setInfo,
info,
}: {
id: string;
setInfo: ({
title,
description,
}: {
title: string;
description: string;
}) => void;
info: {
title: string;
description: string;
};
}) {
const [open, setOpen] = useState(false);
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button className="flex gap-2 border-border" variant="outline">
<PencilSquareIcon className="size-8 stroke-1" /> Edit
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-[425px] bg-[#161f2a]/30 border-0">
<form
action={async (FormData) => {
const data = {
title: FormData.get("title") as string,
description: FormData.get("description") as string,
};
const res = await AddCanvasInfo({ id, ...data });
if (res.success) {
setOpen(false);
setInfo(data);
} else {
setOpen(false);
toast.error("Something went wrong.", {
style: { backgroundColor: "rgb(22 31 42 / 0.3)" },
});
}
}}
>
<DialogHeader>
<DialogTitle>Edit Canvas</DialogTitle>
<DialogDescription>
Add Description to your canvas. Pro tip: Let AI do the job, as you
add your content into canvas, we will autogenerate your
description.
</DialogDescription>
</DialogHeader>
<div className="grid gap-4 py-4">
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="title" className="text-right">
Title
</Label>
<Input
defaultValue={info.title}
name="title"
id="title"
placeholder="life planning..."
className="col-span-3 border-0"
/>
</div>
<div className="grid grid-cols-4 items-center gap-4">
<Label htmlFor="description" className="text-right">
Description
</Label>
<Textarea
defaultValue={info.description}
rows={6}
id="description"
name="description"
placeholder="contains information about..."
className="col-span-3 border-0 resize-none"
/>
</div>
</div>
<DialogFooter>
<Button type="submit">Save changes</Button>
</DialogFooter>
</form>
</DialogContent>
</Dialog>
);
}
import { Suspense, memo, use, useState } from "react";
import { Box, TldrawImage } from "tldraw";
import { Button } from "@repo/ui/shadcn/button";
import { AddCanvasInfo, deleteCanvas } from "@/app/actions/doers";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@repo/ui/shadcn/dialog";
import { Input } from "@repo/ui/shadcn/input";
import { Textarea } from "@repo/ui/shadcn/textarea";
import { textCardUtil } from "@/components/canvas/textCard";
import { twitterCardUtil } from "@/components/canvas/twitterCard";
const ImageComponent = memo(({ id }: { id: string }) => {
const snapshot = use(getCanvasData(id));
if (snapshot.bounds) {
const pageBounds = new Box(
snapshot.bounds.x,
snapshot.bounds.y,
snapshot.bounds.w,
snapshot.bounds.h,
);
return (
<TldrawImage
shapeUtils={[twitterCardUtil, textCardUtil]}
snapshot={snapshot.snapshot}
background={false}
darkMode={true}
bounds={pageBounds}
padding={0}
scale={1}
format="png"
/>
);
}
return (
<div className=" h-full w-full flex justify-center items-center">
Drew things to seee here
</div>
);
});

View file

@ -0,0 +1,32 @@
"use client";
import { motion } from "framer-motion";
import ThinkPad from "./thinkPad";
const containerVariants = {
hidden: { opacity: 0 },
visible: {
opacity: 1,
transition: {
staggerChildren: 0.1,
},
},
};
export default function ThinkPads({
data,
}: {
data: { image: string; title: string; description: string; id: string }[];
}) {
return (
<motion.div
variants={containerVariants}
initial="hidden"
animate="visible"
className="w-[90%] max-w-2xl space-y-6"
>
{data.map((item) => {
return <ThinkPad {...item} />;
})}
</motion.div>
);
}

View file

@ -1,28 +1,36 @@
.tl-background {
background: #1F2428 !important;
background: #1f2428 !important;
}
.tlui-style-panel.tlui-style-panel__wrapper, .tlui-navigation-panel::before ,.tlui-menu-zone, .tlui-toolbar__tools, .tlui-popover__content, .tlui-menu, .tlui-button__help, .tlui-help-menu, .tlui-dialog__content {
background: #2C3439 !important;
border-top: #2C3439 !important;
border-right: #2C3439 !important;
border-bottom: #2C3439 !important;
border-left: #2C3439 !important;
.tlui-style-panel.tlui-style-panel__wrapper,
.tlui-navigation-panel::before,
.tlui-menu-zone,
.tlui-toolbar__tools,
.tlui-popover__content,
.tlui-menu,
.tlui-button__help,
.tlui-help-menu,
.tlui-dialog__content {
background: #2c3840 !important;
border-top: #2c3840 !important;
border-right: #2c3840 !important;
border-bottom: #2c3840 !important;
border-left: #2c3840 !important;
}
.tlui-navigation-panel::before {
border-top: #2C3439 !important;
border-right: #2C3439 !important;
border-top: #2c3840 !important;
border-right: #2c3840 !important;
}
.tlui-minimap {
background: #2C3439 !important;
background: #2c3840 !important;
}
.tlui-minimap__canvas {
background: #1F2428 !important;
background: #1f2428 !important;
}
.tlui-dialog__overlay {
position: fixed;
}
}

View file

@ -0,0 +1,30 @@
import { auth } from "@/server/auth";
import "./canvasStyles.css";
import { redirect } from "next/navigation";
import BackgroundPlus from "../(landing)/GridPatterns/PlusGrid";
import { Toaster } from "@repo/ui/shadcn/sonner";
export default async function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
const info = await auth();
if (!info) {
return redirect("/signin");
}
return (
<>
<div className="relative flex justify-center z-40 pointer-events-none">
<div
className="absolute -z-10 left-0 top-[10%] h-32 w-[90%] overflow-x-hidden bg-[rgb(54,157,253)] bg-opacity-100 md:bg-opacity-70 blur-[337.4px]"
style={{ transform: "rotate(-30deg)" }}
/>
</div>
<BackgroundPlus className="absolute top-0 left-0 w-full h-full -z-50 opacity-70" />
<div>{children}</div>
<Toaster />
</>
);
}

View file

@ -3,6 +3,7 @@
import { revalidatePath } from "next/cache";
import { db } from "../../server/db";
import {
canvas,
chatHistory,
chatThreads,
contentToSpace,
@ -435,3 +436,116 @@ export const linkTelegramToUser = async (
data: true,
};
};
export const createCanvas = async () => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
redirect("/signin");
return { error: "Not authenticated", success: false };
}
const canvases = await db
.select()
.from(canvas)
.where(eq(canvas.userId, data.user.id));
if (canvases.length >= 5) {
return {
success: false,
message: "A user currently can only have 5 canvases",
};
}
const resp = await db
.insert(canvas)
.values({ userId: data.user.id })
.returning({ id: canvas.id });
redirect(`/canvas/${resp[0]!.id}`);
// TODO INVESTIGATE: NO REDIRECT INSIDE TRY CATCH BLOCK
// try {
// const resp = await db
// .insert(canvas)
// .values({ userId: data.user.id }).returning({id: canvas.id});
// return redirect(`/canvas/${resp[0]!.id}`);
// } catch (e: unknown) {
// const error = e as Error;
// if (
// error.message.includes("D1_ERROR: UNIQUE constraint failed: space.name")
// ) {
// return { success: false, data: 0, error: "Space already exists" };
// } else {
// return {
// success: false,
// data: 0,
// error: "Failed to create space with error: " + error.message,
// };
// }
// }
};
export const SaveCanvas = async ({
id,
data,
}: {
id: string;
data: string;
}) => {
console.log({ id, data });
try {
await process.env.CANVAS_SNAPS.put(id, data);
return {
success: true,
message: "in-sync",
};
} catch (error) {
return {
success: false,
error,
message: "An error occured while saving your canvas",
};
}
};
export const deleteCanvas = async (id: string) => {
try {
await process.env.CANVAS_SNAPS.delete(id);
await db.delete(canvas).where(eq(canvas.id, id));
return {
success: true,
message: "in-sync",
};
} catch (error) {
return {
success: false,
error,
message: "An error occured while saving your canvas",
};
}
};
export async function AddCanvasInfo({
id,
title,
description,
}: {
id: string;
title: string;
description: string;
}) {
try {
await db
.update(canvas)
.set({ description, title })
.where(eq(canvas.id, id));
return {
success: true,
message: "info updated successfully",
};
} catch (error) {
return {
success: false,
message: "something went wrong :/",
};
}
}

View file

@ -3,6 +3,7 @@
import { and, asc, eq, inArray, not, sql } from "drizzle-orm";
import { db } from "../../server/db";
import {
canvas,
chatHistory,
ChatThread,
chatThreads,
@ -223,3 +224,71 @@ export const getNoteFromId = async (
data: note,
};
};
export const getCanvas = async () => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
redirect("/signin");
return { error: "Not authenticated", success: false };
}
try {
const canvases = await db
.select()
.from(canvas)
.where(eq(canvas.userId, data.user.id));
return {
success: true,
data: canvases.map(({ userId, ...rest }) => rest),
};
} catch (e) {
return {
success: false,
error: (e as Error).message,
};
}
};
export const userHasCanvas = async (canvasId: string) => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
redirect("/signin");
return { error: "Not authenticated", success: false };
}
try {
const canvases = await db
.select()
.from(canvas)
.where(eq(canvas.userId, data.user.id));
const exists = !!canvases.find((canvas) => canvas.id === canvasId);
return {
success: exists,
};
} catch (e) {
return {
success: false,
error: (e as Error).message,
};
}
};
export const getCanvasData = async (canvasId: string) => {
const data = await auth();
if (!data || !data.user || !data.user.id) {
redirect("/signin");
return { error: "Not authenticated", success: false };
}
const canvas = await process.env.CANVAS_SNAPS.get(canvasId);
console.log({ canvas, canvasId });
if (canvas) {
return JSON.parse(canvas);
} else {
return { snapshot: {} };
}
};

View file

@ -0,0 +1,10 @@
export function GET(req: Request) {
const id = new URL(req.url).searchParams.get("id");
return new Response(JSON.stringify(id));
}
export async function POST(req: Request) {
const body = await req.json();
const id = new URL(req.url).searchParams.get("id");
return new Response(JSON.stringify({ body, id }));
}

View file

@ -0,0 +1,31 @@
import type { NextRequest } from "next/server";
import { ensureAuth } from "../ensureAuth";
export const runtime = "edge";
export async function POST(request: NextRequest) {
const session = await ensureAuth(request);
if (!session) {
return new Response("Unauthorized", { status: 401 });
}
const res: { query: string } = await request.json();
try {
const resp = await fetch(
`${process.env.BACKEND_BASE_URL}/api/search?query=${res.query}&user=${session.user.id}`,
);
if (resp.status !== 200 || !resp.ok) {
const errorData = await resp.text();
console.log(errorData);
return new Response(
JSON.stringify({ message: "Error in CF function", error: errorData }),
{ status: resp.status },
);
}
return new Response(
JSON.stringify({ response: await resp.json(), status: 200 }),
);
} catch (error) {
return new Response(`Error, ${error}`);
}
}

View file

@ -4,13 +4,19 @@ declare global {
GOOGLE_CLIENT_ID: string;
GOOGLE_CLIENT_SECRET: string;
AUTH_SECRET: string;
R2_ENDPOINT: string;
R2_ACCESS_KEY_ID: string;
R2_SECRET_ACCESS_KEY: string;
R2_PUBLIC_BUCKET_ADDRESS: string;
R2_BUCKET_NAME: string;
BACKEND_SECURITY_KEY: string;
BACKEND_BASE_URL: string;
CLOUDFLARE_ACCOUNT_ID: string;
CLOUDFLARE_DATABASE_ID: string;
CLOUDFLARE_D1_TOKEN: string;
}
}
}

View file

@ -1,17 +1,18 @@
import { useCallback, useEffect, useRef, useState } from "react";
import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw";
import { createAssetFromUrl } from "../lib/createAssetUrl";
import { createAssetFromUrl } from "../../lib/createAssetUrl";
import "tldraw/tldraw.css";
import { components } from "./enabledComp";
import { twitterCardUtil } from "./twitterCard";
import { textCardUtil } from "./textCard";
import createEmbedsFromUrl from "../lib/createEmbeds";
import { loadRemoteSnapshot } from "../lib/loadSnap";
import createEmbedsFromUrl from "../../lib/createEmbeds";
import { loadRemoteSnapshot } from "../../lib/loadSnap";
import { SaveStatus } from "./savesnap";
import { getAssetUrls } from "@tldraw/assets/selfHosted";
import { memo } from "react";
import DragContext from "../lib/context";
import DragContext from "../../lib/context";
import DropZone from "./dropComponent";
import { useRect } from "./resizableLayout";
// import "./canvas.css";
export const Canvas = memo(() => {
@ -46,12 +47,13 @@ export const Canvas = memo(() => {
});
const TldrawComponent = memo(() => {
const { id } = useRect();
const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
status: "loading",
});
useEffect(() => {
const fetchStore = async () => {
const store = await loadRemoteSnapshot();
const store = await loadRemoteSnapshot(id);
setStoreWithStatus({
store: store,
@ -71,7 +73,7 @@ const TldrawComponent = memo(() => {
});
}, []);
setUserPreferences({ id: "supermemory" });
setUserPreferences({ id: "supermemory", colorScheme: "dark" });
const assetUrls = getAssetUrls();
return (
@ -85,7 +87,7 @@ const TldrawComponent = memo(() => {
onMount={handleMount}
>
<div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
<SaveStatus />
<SaveStatus id={id} />
</div>
<DropZone />
</Tldraw>

View file

@ -1,40 +1,23 @@
import Image from "next/image";
import { useRef, useState } from "react";
interface DraggableComponentsProps {
content: string;
extraInfo?: string;
icon: string;
iconAlt: string;
}
import { motion } from "framer-motion";
export default function DraggableComponentsContainer({
content,
}: {
content: DraggableComponentsProps[];
content: { context: string }[] | undefined;
}) {
if (content === undefined) return null;
return (
<div className="flex flex-col gap-10">
{content.map((i) => {
return (
<DraggableComponents
content={i.content}
icon={i.icon}
iconAlt={i.iconAlt}
extraInfo={i.extraInfo}
/>
);
return <DraggableComponents content={i.context} />;
})}
</div>
);
}
function DraggableComponents({
content,
extraInfo,
icon,
iconAlt,
}: DraggableComponentsProps) {
function DraggableComponents({ content }: { content: string }) {
const [isDragging, setIsDragging] = useState(false);
const containerRef = useRef<HTMLDivElement>(null);
@ -52,20 +35,21 @@ function DraggableComponents({
};
return (
<div
<motion.div
initial={{ opacity: 0, y: 5 }}
animate={{ opacity: 1, y: 0 }}
ref={containerRef}
onDragEnd={handleDragEnd}
onDragStart={handleDragStart}
draggable
className={`flex gap-4 px-1 rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
className={`flex gap-4 px-3 overflow-hidden rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
>
<Image className="select-none" src={icon} alt={iconAlt} />
<div className="flex flex-col gap-2">
<div>
<h1 className="line-clamp-3">{content}</h1>
</div>
<p className="line-clamp-1 text-[#369DFD]">{extraInfo}</p>
{/* <p className="line-clamp-1 text-[#369DFD]">{extraInfo}</p> */}
</div>
</div>
</motion.div>
);
}

View file

@ -1,7 +1,10 @@
import React, { useRef, useCallback, useEffect, useContext } from "react";
import { useEditor } from "tldraw";
import DragContext, { DragContextType, useDragContext } from "../lib/context";
import { handleExternalDroppedContent } from "../lib/createEmbeds";
import DragContext, {
DragContextType,
useDragContext,
} from "../../lib/context";
import { handleExternalDroppedContent } from "../../lib/createEmbeds";
const stripHtmlTags = (html: string): string => {
const div = document.createElement("div");

View file

@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = {
// HelperButtons: null,
// SharePanel: null,
// MenuPanel: null,
};
};

View file

@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = {
// HelperButtons: null,
// SharePanel: null,
// MenuPanel: null,
};
};

View file

@ -0,0 +1,175 @@
"use client";
import { Canvas } from "./canvas";
import React, { createContext, useContext, useState } from "react";
import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
import { SettingsIcon, DragIcon } from "@repo/ui/icons";
import DraggableComponentsContainer from "./draggableComponent";
import Image from "next/image";
import { Label } from "@repo/ui/shadcn/label";
interface RectContextType {
fullScreen: boolean;
setFullScreen: React.Dispatch<React.SetStateAction<boolean>>;
visible: boolean;
setVisible: React.Dispatch<React.SetStateAction<boolean>>;
id: string;
}
const RectContext = createContext<RectContextType | undefined>(undefined);
export const RectProvider = ({
id,
children,
}: {
id: string;
children: React.ReactNode;
}) => {
const [fullScreen, setFullScreen] = useState(false);
const [visible, setVisible] = useState(true);
const value = {
id,
fullScreen,
setFullScreen,
visible,
setVisible,
};
return <RectContext.Provider value={value}>{children}</RectContext.Provider>;
};
export const useRect = () => {
const context = useContext(RectContext);
if (context === undefined) {
throw new Error("useRect must be used within a RectProvider");
}
return context;
};
export function ResizaleLayout() {
const { setVisible, fullScreen, setFullScreen } = useRect();
return (
<div
className={`h-screen w-full ${!fullScreen ? "px-4 py-6" : "bg-[#1F2428]"} transition-all`}
>
<PanelGroup
onLayout={(l) => {
l[0]! < 20 ? setVisible(false) : setVisible(true);
}}
className={` ${fullScreen ? "w-[calc(100vw-2rem)]" : "w-screen"} transition-all`}
direction="horizontal"
>
<Panel
onExpand={() => {
setTimeout(() => setFullScreen(false), 50);
}}
onCollapse={() => {
setTimeout(() => setFullScreen(true), 50);
}}
defaultSize={30}
collapsible={true}
>
<SidePanelContainer />
</Panel>
<PanelResizeHandle
className={`relative flex items-center transition-all justify-center ${!fullScreen && "px-1"}`}
>
<DragIconContainer />
</PanelResizeHandle>
<Panel className="relative" defaultSize={70} minSize={60}>
<CanvasContainer />
</Panel>
</PanelGroup>
</div>
);
}
function DragIconContainer() {
const { fullScreen } = useRect();
return (
<div
className={`rounded-lg bg-[#2F363B] ${!fullScreen && "px-1"} transition-all py-2`}
>
<Image src={DragIcon} alt="drag-icon" />
</div>
);
}
function CanvasContainer() {
const { fullScreen } = useRect();
return (
<div
className={`absolute overflow-hidden transition-all inset-0 ${fullScreen ? "h-screen " : "h-[calc(100vh-3rem)] rounded-2xl"} w-full`}
>
<Canvas />
</div>
);
}
function SidePanelContainer() {
const { fullScreen, visible } = useRect();
return (
<div
className={`flex transition-all rounded-2xl ${fullScreen ? "h-screen" : "h-[calc(100vh-3rem)]"} w-full flex-col overflow-hidden bg-[#1F2428]`}
>
<div className="flex items-center justify-between bg-[#2C3439] px-4 py-2 text-lg font-medium text-[#989EA4]">
Change Filters
<Image src={SettingsIcon} alt="setting-icon" />
</div>
{visible ? (
<SidePanel />
) : (
<h1 className="text-center py-10 text-xl">Need more space to show!</h1>
)}
</div>
);
}
function SidePanel() {
const [content, setContent] = useState<{ context: string }[]>();
return (
<>
<div className="px-3 py-5">
<form
action={async (FormData) => {
const search = FormData.get("search");
console.log(search);
const res = await fetch("/api/canvasai", {
method: "POST",
body: JSON.stringify({ query: search }),
});
const t = await res.json();
console.log(t.response.response);
setContent(t.response.response);
}}
>
<input
placeholder="search..."
name="search"
className="w-full resize-none rounded-xl bg-[#151515] px-3 py-4 text-xl text-[#989EA4] outline-none focus:outline-none sm:max-h-52"
/>
</form>
</div>
<DraggableComponentsContainer content={content} />
</>
);
}
const content = [
{
content:
"Regional growth patterns diverge, with strong performance in the United States and several emerging markets, contrasted by weaker prospects in many advanced economies, particularly in Europe (World Economic Forum) (OECD). The rapid adoption of artificial intelligence (AI) is expected to drive productivity growth, especially in advanced economies, potentially mitigating labor shortages and boosting income levels in emerging markets (World Economic Forum) (OECD). However, ongoing geopolitical tensions and economic fragmentation are likely to maintain a level of uncertainty and volatility in the global economy (World Economic Forum.",
iconAlt: "Autocomplete",
extraInfo:
"Page Url: https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a",
},
{
content:
"As of mid-2024, the global economy is experiencing modest growth with significant regional disparities. Global GDP growth is projected to be around 3.1% in 2024, rising slightly to 3.2% in 2025. This performance, although below the pre-pandemic average, reflects resilience despite various economic pressures, including tight monetary conditions and geopolitical tensions (IMF)(OECD) Inflation is moderating faster than expected, with global headline inflation projected to fall to 5.8% in 2024 and 4.4% in 2025, contributing to improving real incomes and positive trade growth (IMF) (OECD)",
iconAlt: "Autocomplete",
extraInfo:
"Page Url: https://www.cnbc.com/2024/05/23/nvidia-keeps-hitting-records-can-investors-still-buy-the-stock.html?&qsearchterm=nvidia",
},
];

View file

@ -1,27 +1,19 @@
import { useCallback, useEffect, useState } from "react";
import { debounce, useEditor } from "tldraw";
import { debounce, getSnapshot, useEditor } from "tldraw";
import { SaveCanvas } from "@/app/actions/doers";
export function SaveStatus() {
export function SaveStatus({ id }: { id: string }) {
const [save, setSave] = useState("saved!");
const editor = useEditor();
const debouncedSave = useCallback(
debounce(async () => {
const snapshot = editor.store.getSnapshot();
localStorage.setItem("saved", JSON.stringify(snapshot));
const snapshot = getSnapshot(editor.store);
const bounds = editor.getViewportPageBounds();
console.log(bounds);
const res = await fetch(
"https://learning-cf.pruthvirajthinks.workers.dev/post/page3",
{
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
data: snapshot,
}),
},
);
SaveCanvas({ id, data: JSON.stringify({ snapshot, bounds }) });
console.log(await res.json());
setSave("saved!");
}, 3000),
[editor], // Dependency array ensures the function is not recreated on every render
@ -40,4 +32,4 @@ export function SaveStatus() {
}, [editor, debouncedSave]);
return <button>{save}</button>;
}
}

View file

@ -25,9 +25,11 @@ export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> {
height: s.props.h,
width: s.props.w,
pointerEvents: "all",
background: "#2C3439",
background: "#2E3C4C",
borderRadius: "16px",
border: "2px solid #3e4449",
padding: "8px 14px",
overflow: "auto",
}}
>
<h1 style={{ fontSize: "15px" }}>{s.props.content}</h1>

View file

@ -1,4 +1,9 @@
import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, toDomPrecision } from "tldraw";
import {
BaseBoxShapeUtil,
HTMLContainer,
TLBaseShape,
toDomPrecision,
} from "tldraw";
type ITwitterCardShape = TLBaseShape<
"Twittercard",

1
apps/web/env.d.ts vendored
View file

@ -5,4 +5,5 @@ interface CloudflareEnv {
STORAGE: R2Bucket;
DATABASE: D1Database;
DEV_IMAGES: R2Bucket;
CANVAS_SNAPS: KVNamespace;
}

View file

@ -1,4 +1,4 @@
import { createContext, useContext } from 'react';
import { createContext, useContext } from "react";
export interface DragContextType {
isDraggingOver: boolean;
@ -10,9 +10,9 @@ const DragContext = createContext<DragContextType | undefined>(undefined);
export const useDragContext = () => {
const context = useContext(DragContext);
if (context === undefined) {
throw new Error('useAppContext must be used within an AppProvider');
throw new Error("useAppContext must be used within an AppProvider");
}
return context;
};
export default DragContext;
export default DragContext;

14
apps/web/lib/loadSnap.ts Normal file
View file

@ -0,0 +1,14 @@
import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw";
import { getCanvasData } from "../app/actions/fetchers";
import { twitterCardUtil } from "../components/canvas/twitterCard";
import { textCardUtil } from "../components/canvas/textCard";
export async function loadRemoteSnapshot(id: string) {
const snapshot = await getCanvasData(id);
const newStore = createTLStore({
shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil],
});
loadSnapshot(newStore, snapshot.snapshot);
return newStore;
}

View file

@ -27,6 +27,15 @@ CREATE TABLE `authenticator` (
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `canvas` (
`id` text PRIMARY KEY NOT NULL,
`title` text DEFAULT 'Untitled' NOT NULL,
`description` text DEFAULT 'Untitled' NOT NULL,
`url` text DEFAULT '' NOT NULL,
`userId` text NOT NULL,
FOREIGN KEY (`userId`) REFERENCES `user`(`id`) ON UPDATE no action ON DELETE cascade
);
--> statement-breakpoint
CREATE TABLE `chatHistory` (
`id` integer PRIMARY KEY AUTOINCREMENT NOT NULL,
`threadId` text NOT NULL,
@ -86,7 +95,8 @@ CREATE TABLE `user` (
`name` text,
`email` text NOT NULL,
`emailVerified` integer,
`image` text
`image` text,
`telegramId` text
);
--> statement-breakpoint
CREATE TABLE `verificationToken` (
@ -97,6 +107,7 @@ CREATE TABLE `verificationToken` (
);
--> statement-breakpoint
CREATE UNIQUE INDEX `authenticator_credentialID_unique` ON `authenticator` (`credentialID`);--> statement-breakpoint
CREATE INDEX `canvas_user_userId` ON `canvas` (`userId`);--> statement-breakpoint
CREATE INDEX `chatHistory_thread_idx` ON `chatHistory` (`threadId`);--> statement-breakpoint
CREATE INDEX `chatThread_user_idx` ON `chatThread` (`userId`);--> statement-breakpoint
CREATE UNIQUE INDEX `space_name_unique` ON `space` (`name`);--> statement-breakpoint
@ -105,4 +116,7 @@ CREATE INDEX `spaces_user_idx` ON `space` (`user`);--> statement-breakpoint
CREATE INDEX `storedContent_url_idx` ON `storedContent` (`url`);--> statement-breakpoint
CREATE INDEX `storedContent_savedAt_idx` ON `storedContent` (`savedAt`);--> statement-breakpoint
CREATE INDEX `storedContent_title_idx` ON `storedContent` (`title`);--> statement-breakpoint
CREATE INDEX `storedContent_user_idx` ON `storedContent` (`user`);
CREATE INDEX `storedContent_user_idx` ON `storedContent` (`user`);--> statement-breakpoint
CREATE INDEX `users_email_idx` ON `user` (`email`);--> statement-breakpoint
CREATE INDEX `users_telegram_idx` ON `user` (`telegramId`);--> statement-breakpoint
CREATE INDEX `users_id_idx` ON `user` (`id`);

View file

@ -1,7 +1,7 @@
{
"version": "6",
"dialect": "sqlite",
"id": "6988522d-8117-484d-b52a-94c0fbd75140",
"id": "58ec8bd8-5aad-4ade-a718-74eaf6056d36",
"prevId": "00000000-0000-0000-0000-000000000000",
"tables": {
"account": {
@ -191,6 +191,69 @@
},
"uniqueConstraints": {}
},
"canvas": {
"name": "canvas",
"columns": {
"id": {
"name": "id",
"type": "text",
"primaryKey": true,
"notNull": true,
"autoincrement": false
},
"title": {
"name": "title",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Untitled'"
},
"description": {
"name": "description",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "'Untitled'"
},
"url": {
"name": "url",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false,
"default": "''"
},
"userId": {
"name": "userId",
"type": "text",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
"canvas_user_userId": {
"name": "canvas_user_userId",
"columns": ["userId"],
"isUnique": false
}
},
"foreignKeys": {
"canvas_userId_user_id_fk": {
"name": "canvas_userId_user_id_fk",
"tableFrom": "canvas",
"tableTo": "user",
"columnsFrom": ["userId"],
"columnsTo": ["id"],
"onDelete": "cascade",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"chatHistory": {
"name": "chatHistory",
"columns": {
@ -415,13 +478,6 @@
"primaryKey": false,
"notNull": false,
"autoincrement": false
},
"createdAt": {
"name": "createdAt",
"type": "integer",
"primaryKey": false,
"notNull": true,
"autoincrement": false
}
},
"indexes": {
@ -681,5 +737,8 @@
"schemas": {},
"tables": {},
"columns": {}
},
"internal": {
"indexes": {}
}
}

View file

@ -1,26 +1,12 @@
{
"version": "6",
"version": "7",
"dialect": "sqlite",
"entries": [
{
"idx": 0,
"version": "6",
"when": 1719671614406,
"tag": "0000_classy_speed_demon",
"breakpoints": true
},
{
"idx": 1,
"version": "6",
"when": 1719772835765,
"tag": "0001_chubby_vulture",
"breakpoints": true
},
{
"idx": 2,
"version": "6",
"when": 1719789157348,
"tag": "0002_old_titanium_man",
"when": 1719619409551,
"tag": "0000_bitter_electro",
"breakpoints": true
}
]

View file

@ -201,5 +201,24 @@ export const chatHistory = createTable(
}),
);
export const canvas = createTable(
"canvas",
{
id: text("id")
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
title: text("title").default("Untitled").notNull(),
description: text("description").default("Untitled").notNull(),
imageUrl: text("url").default("").notNull(),
userId: text("userId")
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
},
(canvas) => ({
userIdx: index("canvas_user_userId").on(canvas.userId),
}),
);
export type ChatThread = typeof chatThreads.$inferSelect;
export type ChatHistory = typeof chatHistory.$inferSelect;

View file

@ -18,7 +18,8 @@
"**/*.tsx",
".next/types/**/*.ts",
"../../packages/ui/",
"../../packages/shared-types/utils.ts"
"../../packages/shared-types/utils.ts",
"./components"
],
"exclude": ["node_modules/"]
}

View file

@ -16,6 +16,10 @@ 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"

View file

@ -1,14 +0,0 @@
import { createTLStore, defaultShapeUtils, loadSnapshot } from "tldraw";
import { twitterCardUtil } from "../components/twitterCard";
import { textCardUtil } from "../components/textCard";
export async function loadRemoteSnapshot() {
const res = await fetch(
"https://learning-cf.pruthvirajthinks.workers.dev/get/page3",
);
const snapshot = JSON.parse(await res.json());
const newStore = createTLStore({
shapeUtils: [...defaultShapeUtils, twitterCardUtil, textCardUtil],
});
loadSnapshot(newStore, snapshot);
return newStore;
}