mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-08-13 02:24:34 +00:00
merge conflicts
This commit is contained in:
commit
ffd141ade4
34 changed files with 1012 additions and 256 deletions
|
|
@ -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",
|
||||
},
|
||||
];
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
|
|
|||
45
apps/web/app/(canvas)/canvas/search&create.tsx
Normal file
45
apps/web/app/(canvas)/canvas/search&create.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
276
apps/web/app/(canvas)/canvas/thinkPad.tsx
Normal file
276
apps/web/app/(canvas)/canvas/thinkPad.tsx
Normal 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>
|
||||
);
|
||||
});
|
||||
32
apps/web/app/(canvas)/canvas/thinkPads.tsx
Normal file
32
apps/web/app/(canvas)/canvas/thinkPads.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
30
apps/web/app/(canvas)/layout.tsx
Normal file
30
apps/web/app/(canvas)/layout.tsx
Normal 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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 :/",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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: {} };
|
||||
}
|
||||
};
|
||||
|
|
|
|||
10
apps/web/app/api/canvas/route.ts
Normal file
10
apps/web/app/api/canvas/route.ts
Normal 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 }));
|
||||
}
|
||||
31
apps/web/app/api/canvasai/route.ts
Normal file
31
apps/web/app/api/canvasai/route.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
6
apps/web/cf-env.d.ts
vendored
6
apps/web/cf-env.d.ts
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = {
|
|||
// HelperButtons: null,
|
||||
// SharePanel: null,
|
||||
// MenuPanel: null,
|
||||
};
|
||||
};
|
||||
|
|
@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = {
|
|||
// HelperButtons: null,
|
||||
// SharePanel: null,
|
||||
// MenuPanel: null,
|
||||
};
|
||||
};
|
||||
175
apps/web/components/canvas/resizableLayout.tsx
Normal file
175
apps/web/components/canvas/resizableLayout.tsx
Normal 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",
|
||||
},
|
||||
];
|
||||
|
|
@ -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>;
|
||||
}
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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
1
apps/web/env.d.ts
vendored
|
|
@ -5,4 +5,5 @@ interface CloudflareEnv {
|
|||
STORAGE: R2Bucket;
|
||||
DATABASE: D1Database;
|
||||
DEV_IMAGES: R2Bucket;
|
||||
CANVAS_SNAPS: KVNamespace;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
14
apps/web/lib/loadSnap.ts
Normal 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;
|
||||
}
|
||||
|
|
@ -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`);
|
||||
|
|
@ -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": {}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -18,7 +18,8 @@
|
|||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
"../../packages/ui/",
|
||||
"../../packages/shared-types/utils.ts"
|
||||
"../../packages/shared-types/utils.ts",
|
||||
"./components"
|
||||
],
|
||||
"exclude": ["node_modules/"]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue