aboutsummaryrefslogtreecommitdiff
path: root/apps/web/components
diff options
context:
space:
mode:
authorcodetorso <[email protected]>2024-07-25 10:56:32 +0530
committercodetorso <[email protected]>2024-07-25 10:56:32 +0530
commitc7b98a39b8024c96f1230a513a6d64d763b74277 (patch)
tree0bc3260cfce7cbf5666948f21864806b6d2a9760 /apps/web/components
parentparse suggestions for handling edge case (diff)
downloadsupermemory-c7b98a39b8024c96f1230a513a6d64d763b74277.tar.xz
supermemory-c7b98a39b8024c96f1230a513a6d64d763b74277.zip
let's go boys!! canvas
Diffstat (limited to 'apps/web/components')
-rw-r--r--apps/web/components/canvas/canvas.tsx96
-rw-r--r--apps/web/components/canvas/custom_nodes/textcard.tsx99
-rw-r--r--apps/web/components/canvas/custom_nodes/twittercard.tsx (renamed from apps/web/components/canvas/twitterCard.tsx)0
-rw-r--r--apps/web/components/canvas/draggableComponent.tsx56
-rw-r--r--apps/web/components/canvas/dropComponent.tsx206
-rw-r--r--apps/web/components/canvas/enabled.tsx (renamed from apps/web/components/canvas/enabledComp.tsx)2
-rw-r--r--apps/web/components/canvas/enabledComp copy.tsx22
-rw-r--r--apps/web/components/canvas/resizableLayout.tsx177
-rw-r--r--apps/web/components/canvas/resizablelayout.tsx46
-rw-r--r--apps/web/components/canvas/savesnap.tsx4
-rw-r--r--apps/web/components/canvas/sidepanel.tsx101
-rw-r--r--apps/web/components/canvas/sidepanelcard.tsx69
-rw-r--r--apps/web/components/canvas/textCard.tsx47
-rw-r--r--apps/web/components/canvas/tldrawComponent.tsx84
-rw-r--r--apps/web/components/canvas/tldrawDrop.tsx67
15 files changed, 469 insertions, 607 deletions
diff --git a/apps/web/components/canvas/canvas.tsx b/apps/web/components/canvas/canvas.tsx
deleted file mode 100644
index 904cad3a..00000000
--- a/apps/web/components/canvas/canvas.tsx
+++ /dev/null
@@ -1,96 +0,0 @@
-import { useCallback, useEffect, useRef, useState } from "react";
-import { Editor, Tldraw, setUserPreferences, TLStoreWithStatus } from "tldraw";
-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 { SaveStatus } from "./savesnap";
-import { getAssetUrls } from "@tldraw/assets/selfHosted";
-import { memo } from "react";
-import DragContext from "../../lib/context";
-import DropZone from "./dropComponent";
-import { useRect } from "./resizableLayout";
-// import "./canvas.css";
-
-export const Canvas = memo(() => {
- const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
- const Dragref = useRef<HTMLDivElement | null>(null);
-
- const handleDragOver = (event: any) => {
- event.preventDefault();
- setIsDraggingOver(true);
- console.log("entere");
- };
-
- useEffect(() => {
- const divElement = Dragref.current;
- if (divElement) {
- divElement.addEventListener("dragover", handleDragOver);
- }
- return () => {
- if (divElement) {
- divElement.removeEventListener("dragover", handleDragOver);
- }
- };
- }, []);
-
- return (
- <DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}>
- <div ref={Dragref} className="w-full h-full">
- <TldrawComponent />
- </div>
- </DragContext.Provider>
- );
-});
-
-const TldrawComponent = memo(() => {
- const { id } = useRect();
- const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
- status: "loading",
- });
- useEffect(() => {
- const fetchStore = async () => {
- const store = await loadRemoteSnapshot(id);
-
- setStoreWithStatus({
- store: store,
- status: "not-synced",
- });
- };
-
- fetchStore();
- }, []);
-
- const handleMount = useCallback((editor: Editor) => {
- (window as any).app = editor;
- (window as any).editor = editor;
- editor.registerExternalAssetHandler("url", createAssetFromUrl);
- editor.registerExternalContentHandler("url", ({ url, point, sources }) => {
- createEmbedsFromUrl({ url, point, sources, editor });
- });
- }, []);
-
- setUserPreferences({ id: "supermemory", colorScheme: "dark" });
-
- const assetUrls = getAssetUrls();
- return (
- <div className="w-full h-full">
- <Tldraw
- className="relative"
- assetUrls={assetUrls}
- components={components}
- store={storeWithStatus}
- shapeUtils={[twitterCardUtil, textCardUtil]}
- onMount={handleMount}
- >
- <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
- <SaveStatus id={id} />
- </div>
- <DropZone />
- </Tldraw>
- </div>
- );
-});
diff --git a/apps/web/components/canvas/custom_nodes/textcard.tsx b/apps/web/components/canvas/custom_nodes/textcard.tsx
new file mode 100644
index 00000000..e778eabb
--- /dev/null
+++ b/apps/web/components/canvas/custom_nodes/textcard.tsx
@@ -0,0 +1,99 @@
+import {
+ BaseBoxShapeUtil,
+ HTMLContainer,
+ TLBaseShape,
+ stopEventPropagation,
+} from "tldraw";
+
+type ITextCardShape = TLBaseShape<
+ "Textcard",
+ { w: number; h: number; content: string; extrainfo: string; type: string }
+>;
+
+export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> {
+ static override type = "Textcard" as const;
+
+ getDefaultProps(): ITextCardShape["props"] {
+ return {
+ w: 100,
+ h: 50,
+ content: "",
+ extrainfo: "",
+ type: "",
+ };
+ }
+
+ override canEdit = () => true;
+
+ component(s: ITextCardShape) {
+ const isEditing = this.editor.getEditingShapeId() === s.id;
+
+ return (
+ <HTMLContainer
+ onPointerDown={isEditing ? stopEventPropagation : undefined}
+ className="flex h-full w-full items-center justify-center"
+ style={{
+ pointerEvents: isEditing ? "all" : "none",
+ }}
+ >
+ <div
+ className="overflow-hidden"
+ style={{
+ height: s.props.h,
+ width: s.props.w,
+ pointerEvents: "all",
+ background: "#232c2f",
+ borderRadius: "16px",
+ border: "2px solid #374151",
+ padding: "8px 14px",
+ }}
+ >
+ <h2 style={{ color: "#95A0AB" }}>{s.props.type}</h2>
+ {isEditing ? (
+ <input
+ value={s.props.content}
+ onChange={(e) =>
+ this.editor.updateShape<ITextCardShape>({
+ id: s.id,
+ type: "Textcard",
+ props: { content: e.currentTarget.value },
+ })
+ }
+ onPointerDown={(e) => {e.stopPropagation()}}
+ onTouchStart={(e) => {e.stopPropagation();}}
+ onTouchEnd={(e) => {e.stopPropagation();}}
+ className="bg-transparent block w-full text-lg font-medium border-[1px] border-[#556970]"
+ type="text"
+ />
+ ) : (
+ <h1 className="text-lg font-medium">{s.props.content}</h1>
+ )}
+ {isEditing ? (
+ <textarea
+ value={s.props.extrainfo}
+ onChange={(e) =>
+ this.editor.updateShape<ITextCardShape>({
+ id: s.id,
+ type: "Textcard",
+ props: { extrainfo: e.currentTarget.value },
+ })
+ }
+ onPointerDown={(e) => {e.stopPropagation();}}
+ onTouchStart={(e) => {e.stopPropagation()}}
+ onTouchEnd={(e) => {e.stopPropagation();}}
+ className="bg-transparent h-full w-full text-base font-medium border-[1px] border-[#556970]"
+ />
+ ) : (
+ <p style={{ fontSize: "15px", color: "#e5e7eb" }}>
+ {s.props.extrainfo}
+ </p>
+ )}
+ </div>
+ </HTMLContainer>
+ );
+ }
+
+ indicator(shape: ITextCardShape) {
+ return <rect width={shape.props.w} height={shape.props.h} />;
+ }
+}
diff --git a/apps/web/components/canvas/twitterCard.tsx b/apps/web/components/canvas/custom_nodes/twittercard.tsx
index 6aebf5ff..6aebf5ff 100644
--- a/apps/web/components/canvas/twitterCard.tsx
+++ b/apps/web/components/canvas/custom_nodes/twittercard.tsx
diff --git a/apps/web/components/canvas/draggableComponent.tsx b/apps/web/components/canvas/draggableComponent.tsx
deleted file mode 100644
index 4d72995b..00000000
--- a/apps/web/components/canvas/draggableComponent.tsx
+++ /dev/null
@@ -1,56 +0,0 @@
-import Image from "next/image";
-import { useRef, useState } from "react";
-import { motion } from "framer-motion";
-
-export default function DraggableComponentsContainer({
- content,
-}: {
- content: { context: string }[] | undefined;
-}) {
- if (content === undefined) return null;
- return (
- <div className="flex flex-col gap-10">
- {content.map((i) => {
- return <DraggableComponents content={i.context} />;
- })}
- </div>
- );
-}
-
-function DraggableComponents({ content }: { content: string }) {
- const [isDragging, setIsDragging] = useState(false);
- const containerRef = useRef<HTMLDivElement>(null);
-
- const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
- setIsDragging(true);
- if (containerRef.current) {
- // Serialize the children as a string for dataTransfer
- const childrenHtml = containerRef.current.innerHTML;
- event.dataTransfer.setData("text/html", childrenHtml);
- }
- };
-
- const handleDragEnd = () => {
- setIsDragging(false);
- };
-
- return (
- <motion.div
- initial={{ opacity: 0, y: 5 }}
- animate={{ opacity: 1, y: 0 }}
- ref={containerRef}
- onDragEnd={handleDragEnd}
- // @ts-expect-error TODO: fix this
- onDragStart={handleDragStart}
- draggable
- className={`flex gap-4 px-3 overflow-hidden rounded-md text-[#989EA4] border-2 transition ${isDragging ? "border-blue-600" : "border-[#1F2428]"}`}
- >
- <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> */}
- </div>
- </motion.div>
- );
-}
diff --git a/apps/web/components/canvas/dropComponent.tsx b/apps/web/components/canvas/dropComponent.tsx
deleted file mode 100644
index df7a55ad..00000000
--- a/apps/web/components/canvas/dropComponent.tsx
+++ /dev/null
@@ -1,206 +0,0 @@
-import React, { useRef, useCallback, useEffect, useContext } from "react";
-import { useEditor } from "tldraw";
-import DragContext, {
- DragContextType,
- useDragContext,
-} from "../../lib/context";
-import { handleExternalDroppedContent } from "../../lib/createEmbeds";
-
-const stripHtmlTags = (html: string): string => {
- const div = document.createElement("div");
- div.innerHTML = html;
- return div.textContent || div.innerText || "";
-};
-
-function formatTextToRatio(text: string) {
- const totalWidth = text.length;
- const maxLineWidth = Math.floor(totalWidth / 4);
-
- const words = text.split(" ");
- let lines = [];
- let currentLine = "";
-
- words.forEach((word) => {
- // Check if adding the next word exceeds the maximum line width
- if ((currentLine + word).length <= maxLineWidth) {
- currentLine += (currentLine ? " " : "") + word;
- } else {
- // If the current line is full, push it to new line
- lines.push(currentLine);
- currentLine = word;
- }
- });
- if (currentLine) {
- lines.push(currentLine);
- }
- return lines.join("\n");
-}
-
-function DropZone() {
- const dropRef = useRef<HTMLDivElement | null>(null);
- const { isDraggingOver, setIsDraggingOver } = useDragContext();
-
- const editor = useEditor();
-
- const handleDragLeave = () => {
- setIsDraggingOver(false);
- console.log("leaver");
- };
-
- useEffect(() => {
- setInterval(() => {
- editor.selectAll();
- const shapes = editor.getSelectedShapes();
- const text = shapes.filter((s) => s.type === "text");
- console.log("hrhh", text);
- }, 5000);
- }, []);
-
- const handleDrop = useCallback((event: DragEvent) => {
- event.preventDefault();
- setIsDraggingOver(false);
- const dt = event.dataTransfer;
- if (!dt) {
- return;
- }
- const items = dt.items;
-
- for (let i = 0; i < items.length; i++) {
- if (items[i]!.kind === "file" && items[i]!.type.startsWith("image/")) {
- const file = items[i]!.getAsFile();
- if (file) {
- const reader = new FileReader();
- reader.onload = (e) => {
- if (e.target) {
- // setDroppedImage(e.target.result as string);
- }
- };
- reader.readAsDataURL(file);
- }
- } else if (items[i]!.kind === "string") {
- items[i]!.getAsString((data) => {
- const cleanText = stripHtmlTags(data);
- const onethree = formatTextToRatio(cleanText);
- handleExternalDroppedContent({ editor, text: onethree });
- });
- }
- }
- }, []);
-
- useEffect(() => {
- const divElement = dropRef.current;
- if (divElement) {
- divElement.addEventListener("drop", handleDrop);
- divElement.addEventListener("dragleave", handleDragLeave);
- }
- return () => {
- if (divElement) {
- divElement.removeEventListener("drop", handleDrop);
- divElement.addEventListener("dragleave", handleDragLeave);
- }
- };
- }, []);
-
- return (
- <div
- className={`h-full flex justify-center items-center w-full absolute top-0 left-0 z-[100000] pointer-events-none ${isDraggingOver && "bg-[#2c3439ad] pointer-events-auto"}`}
- ref={dropRef}
- >
- {isDraggingOver && (
- <>
- <div className="absolute top-4 left-8">
- <TopRight />
- </div>
- <div className="absolute top-4 right-8">
- <TopLeft />
- </div>
- <div className="absolute bottom-4 left-8">
- <BottomLeft />
- </div>
- <div className="absolute bottom-4 right-8">
- <BottomRight />
- </div>
- <h2 className="text-2xl">Drop here to add Content on Canvas</h2>
- </>
- )}
- </div>
- );
-}
-
-function TopRight() {
- return (
- <svg
- width="48"
- height="48"
- viewBox="0 0 48 48"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- d="M44 4H12C7.58172 4 4 7.58172 4 12V44"
- stroke="white"
- stroke-width="8"
- stroke-linecap="round"
- />
- </svg>
- );
-}
-
-function TopLeft() {
- return (
- <svg
- width="48"
- height="48"
- viewBox="0 0 48 48"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- d="M4 4H36C40.4183 4 44 7.58172 44 12V44"
- stroke="white"
- stroke-width="8"
- stroke-linecap="round"
- />
- </svg>
- );
-}
-
-function BottomLeft() {
- return (
- <svg
- width="48"
- height="48"
- viewBox="0 0 48 48"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- d="M44 44H12C7.58172 44 4 40.4183 4 36V4"
- stroke="white"
- stroke-width="8"
- stroke-linecap="round"
- />
- </svg>
- );
-}
-
-function BottomRight() {
- return (
- <svg
- width="48"
- height="48"
- viewBox="0 0 48 48"
- fill="none"
- xmlns="http://www.w3.org/2000/svg"
- >
- <path
- d="M4 44H36C40.4183 44 44 40.4183 44 36V4"
- stroke="white"
- stroke-width="8"
- stroke-linecap="round"
- />
- </svg>
- );
-}
-
-export default DropZone;
diff --git a/apps/web/components/canvas/enabledComp.tsx b/apps/web/components/canvas/enabled.tsx
index e8ed403d..cb68025c 100644
--- a/apps/web/components/canvas/enabledComp.tsx
+++ b/apps/web/components/canvas/enabled.tsx
@@ -19,4 +19,4 @@ export const components: Partial<TLUiComponents> = {
// HelperButtons: null,
// SharePanel: null,
// MenuPanel: null,
-};
+}; \ No newline at end of file
diff --git a/apps/web/components/canvas/enabledComp copy.tsx b/apps/web/components/canvas/enabledComp copy.tsx
deleted file mode 100644
index e8ed403d..00000000
--- a/apps/web/components/canvas/enabledComp copy.tsx
+++ /dev/null
@@ -1,22 +0,0 @@
-import { TLUiComponents } from "tldraw";
-
-export const components: Partial<TLUiComponents> = {
- ActionsMenu: null,
- MainMenu: null,
- QuickActions: null,
- TopPanel: null,
- DebugPanel: null,
- DebugMenu: null,
- PageMenu: null,
- // Minimap: null,
- // ContextMenu: null,
- // HelpMenu: null,
- // ZoomMenu: null,
- // StylePanel: null,
- // NavigationPanel: null,
- // Toolbar: null,
- // KeyboardShortcutsDialog: null,
- // HelperButtons: null,
- // SharePanel: null,
- // MenuPanel: null,
-};
diff --git a/apps/web/components/canvas/resizableLayout.tsx b/apps/web/components/canvas/resizableLayout.tsx
deleted file mode 100644
index de4d124c..00000000
--- a/apps/web/components/canvas/resizableLayout.tsx
+++ /dev/null
@@ -1,177 +0,0 @@
-"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();
- // @ts-expect-error TODO: fix this
- console.log(t.response.response);
- // @ts-expect-error TODO: fix this
- 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",
- },
-];
diff --git a/apps/web/components/canvas/resizablelayout.tsx b/apps/web/components/canvas/resizablelayout.tsx
new file mode 100644
index 00000000..c8f0cc6e
--- /dev/null
+++ b/apps/web/components/canvas/resizablelayout.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { Panel, PanelGroup, PanelResizeHandle } from "react-resizable-panels";
+import TldrawComponent from "./tldrawComponent";
+import Sidepanel from "./sidepanel";
+import Image from "next/image";
+import { DragIcon } from "@repo/ui/icons";
+import { useRef, useState } from "react";
+
+export default function ResizableLayout({ id }: { id: string }) {
+ const panelGroupRef = useRef(null);
+ const [isLeftPanelCollapsed, setIsLeftPanelCollapsed] = useState(false);
+
+ const handleResize = () => {
+ if (isLeftPanelCollapsed && panelGroupRef.current) {
+ panelGroupRef.current.setLayout([20, 80]);
+ }
+ };
+
+ return (
+ <PanelGroup
+ className="text-white h-screen py-[1vh] px-[1vh] bg-[#111417]"
+ direction="horizontal"
+ ref={panelGroupRef}
+ onLayout={(sizes) => {
+ setIsLeftPanelCollapsed(sizes[0] === 0);
+ }}
+ >
+ <Panel collapsible={true} defaultSize={30} minSize={20}>
+ <Sidepanel />
+ </Panel>
+ <PanelResizeHandle onClick={handleResize} className="w-4 h-[98vh] relative">
+ <div
+ className={`rounded-lg bg-[#2F363B] absolute top-1/2 -translate-y-1/2 px-1 transition-all py-2`}
+ >
+ <Image src={DragIcon} alt="drag-icon" />
+ </div>
+ </PanelResizeHandle>
+ <Panel defaultSize={70} minSize={60}>
+ <div className="relative w-full h-[98vh] rounded-xl overflow-hidden">
+ <TldrawComponent id={id} />
+ </div>
+ </Panel>
+ </PanelGroup>
+ );
+}
diff --git a/apps/web/components/canvas/savesnap.tsx b/apps/web/components/canvas/savesnap.tsx
index fd04b3df..713bd3dd 100644
--- a/apps/web/components/canvas/savesnap.tsx
+++ b/apps/web/components/canvas/savesnap.tsx
@@ -16,7 +16,7 @@ export function SaveStatus({ id }: { id: string }) {
setSave("saved!");
}, 3000),
- [editor], // Dependency array ensures the function is not recreated on every render
+ [editor], // ensures the function is not recreated on every render
);
useEffect(() => {
@@ -28,7 +28,7 @@ export function SaveStatus({ id }: { id: string }) {
{ scope: "document", source: "user" },
);
- return () => unsubscribe(); // Cleanup on unmount
+ return () => unsubscribe();
}, [editor, debouncedSave]);
return <button>{save}</button>;
diff --git a/apps/web/components/canvas/sidepanel.tsx b/apps/web/components/canvas/sidepanel.tsx
new file mode 100644
index 00000000..ff5661f8
--- /dev/null
+++ b/apps/web/components/canvas/sidepanel.tsx
@@ -0,0 +1,101 @@
+import React, { useState } from "react";
+import { ArrowLeftIcon, Cog6ToothIcon } from "@heroicons/react/16/solid";
+import Link from "next/link";
+import Card from "./sidepanelcard";
+import { sourcesZod } from "@repo/shared-types";
+import { toast } from "sonner";
+
+type card = {
+ title: string;
+ type: string;
+ source: string;
+ content: string;
+ numChunks: string;
+};
+
+function Sidepanel() {
+ const [content, setContent] = useState<card[]>([]);
+ return (
+ <div className="h-[98vh] bg-[#1f2428] rounded-xl overflow-hidden">
+ <div className="flex justify-between bg-[#2C3439] items-center py-2 px-4 mb-2 text-lg">
+ <Link
+ href="/thinkpad"
+ className="p-2 px-4 transition-colors rounded-lg hover:bg-[#334044] flex items-center gap-2"
+ >
+ <ArrowLeftIcon className="h-5 w-5" />
+ Back
+ </Link>
+ <div className="p-2 px-4 transition-colors rounded-lg hover:bg-[#334044] flex items-center gap-2">
+ <Cog6ToothIcon className="h-5 w-5" />
+ Options
+ </div>
+ </div>
+ <div className="h-full px-2">
+ <div className=" p-2 h-full">
+ <Search setContent={setContent} />
+ <div className="py-5 space-y-4">
+ {content.map((v, i) => (
+ <Card {...v} />
+ ))}
+ </div>
+ </div>
+ </div>
+ </div>
+ );
+}
+
+function Search({ setContent }: { setContent: (e: any) => void }) {
+ return (
+ <form
+ action={async (FormData) => {
+ const search = FormData.get("search") as string;
+
+ const sourcesFetch = await fetch("/api/canvasai", {
+ method: "POST",
+ body: JSON.stringify({ query: search }),
+ });
+
+ const sources = await sourcesFetch.json();
+
+ const sourcesParsed = sourcesZod.safeParse(sources);
+
+ if (!sourcesParsed.success) {
+ console.error(sourcesParsed.error);
+ toast.error("Something went wrong while getting the sources");
+ return;
+ }
+ const filteredSourceUrls = new Set(
+ sourcesParsed.data.metadata.map((source) => source.url),
+ );
+ const uniqueSources = sourcesParsed.data.metadata.filter((source) => {
+ if (filteredSourceUrls.has(source.url)) {
+ filteredSourceUrls.delete(source.url);
+ return true;
+ }
+
+ return false;
+ });
+ setContent(
+ uniqueSources.map((source) => ({
+ title: source.title ?? "Untitled",
+ type: source.type ?? "page",
+ source: source.url ?? "https://supermemory.ai",
+ content: source.description ?? "No content available",
+ numChunks: sourcesParsed.data.metadata.filter(
+ (f) => f.url === source.url,
+ ).length,
+ })),
+ );
+ }}
+ >
+ <input
+ name="search"
+ placeholder="search memories..."
+ className="rounded-md w-full bg-[#121718] p-3 text-lg outline-none"
+ type="text"
+ />
+ </form>
+ );
+}
+
+export default Sidepanel;
diff --git a/apps/web/components/canvas/sidepanelcard.tsx b/apps/web/components/canvas/sidepanelcard.tsx
new file mode 100644
index 00000000..606c7ed8
--- /dev/null
+++ b/apps/web/components/canvas/sidepanelcard.tsx
@@ -0,0 +1,69 @@
+import { GlobeAltIcon } from "@heroicons/react/16/solid";
+import { TwitterIcon, TypeIcon } from "lucide-react";
+import { useState } from "react";
+import { motion } from "framer-motion";
+import useMeasure from "react-use-measure";
+type CardType = string;
+
+export default function Card({
+ type,
+ title,
+ content,
+ source,
+}: {
+ type: CardType;
+ title: string;
+ content: string;
+ source?: string;
+}) {
+ const [ref, bounds] = useMeasure();
+
+ const [isDragging, setIsDragging] = useState(false);
+
+ const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
+ setIsDragging(true);
+ event.dataTransfer.setData(
+ "application/json",
+ JSON.stringify({ type, title, content, source })
+ );
+ };
+
+ const handleDragEnd = () => {
+ setIsDragging(false);
+ };
+
+ return (
+ <motion.div animate={{height: bounds.height}}>
+ <div
+ draggable
+ onDragEnd={handleDragEnd}
+ onDragStart={handleDragStart}
+ className={`rounded-lg hover:scale-[1.02] group cursor-grab scale-[0.98] select-none transition-all border-[#232c2f] hover:bg-[#232c32] ${
+ isDragging ? "border-blue-600 border-dashed border-2" : ""
+ }`}
+ >
+ <div ref={ref} className="flex gap-4 px-3 py-2 items-center">
+ <a href={source} className={`${source && "cursor-pointer"}`}>
+ <Icon type={type} />
+ </a>
+ <div>
+ <h2>{title}</h2>
+ <p className="group-hover:line-clamp-[12] transition-all line-clamp-3 text-gray-200">
+ {content}
+ </p>
+ </div>
+ </div>
+ </div>
+ </motion.div>
+ );
+}
+
+function Icon({ type }: { type: CardType }) {
+ return type === "note" ? (
+ <TypeIcon />
+ ) : type === "page" ? (
+ <GlobeAltIcon className="h-9 w-5" />
+ ) : (
+ <TwitterIcon />
+ );
+}
diff --git a/apps/web/components/canvas/textCard.tsx b/apps/web/components/canvas/textCard.tsx
deleted file mode 100644
index 5f649135..00000000
--- a/apps/web/components/canvas/textCard.tsx
+++ /dev/null
@@ -1,47 +0,0 @@
-import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape } from "tldraw";
-
-type ITextCardShape = TLBaseShape<
- "Textcard",
- { w: number; h: number; content: string; extrainfo: string }
->;
-
-export class textCardUtil extends BaseBoxShapeUtil<ITextCardShape> {
- static override type = "Textcard" as const;
-
- getDefaultProps(): ITextCardShape["props"] {
- return {
- w: 100,
- h: 50,
- content: "",
- extrainfo: "",
- };
- }
-
- component(s: ITextCardShape) {
- return (
- <HTMLContainer className="flex h-full w-full items-center justify-center">
- <div
- style={{
- height: s.props.h,
- width: s.props.w,
- pointerEvents: "all",
- background: "#2E3C4C",
- borderRadius: "16px",
- border: "2px solid #3e4449",
- padding: "8px 14px",
- overflow: "auto",
- }}
- >
- <h1 style={{ fontSize: "15px" }}>{s.props.content}</h1>
- <p style={{ fontSize: "14px", color: "#369DFD" }}>
- {s.props.extrainfo}
- </p>
- </div>
- </HTMLContainer>
- );
- }
-
- indicator(shape: ITextCardShape) {
- return <rect width={shape.props.w} height={shape.props.h} />;
- }
-}
diff --git a/apps/web/components/canvas/tldrawComponent.tsx b/apps/web/components/canvas/tldrawComponent.tsx
new file mode 100644
index 00000000..316722df
--- /dev/null
+++ b/apps/web/components/canvas/tldrawComponent.tsx
@@ -0,0 +1,84 @@
+import React, {
+ createContext,
+ memo,
+ useCallback,
+ useEffect,
+ useState,
+} from "react";
+import { Editor, TLStoreWithStatus, Tldraw, setUserPreferences } from "tldraw";
+import { components } from "./enabled";
+import { twitterCardUtil } from "./custom_nodes/twittercard";
+import { textCardUtil } from "./custom_nodes/textcard";
+import DropZone from "./tldrawDrop";
+import { loadRemoteSnapshot } from "@/lib/loadSnap";
+import { createAssetFromUrl } from "@/lib/createAssetUrl";
+import createEmbedsFromUrl from "@/lib/ExternalDroppedContent";
+import { getAssetUrls } from "@tldraw/assets/selfHosted";
+import { SaveStatus } from "./savesnap";
+
+interface DragContextType {
+ isDraggingOver: boolean;
+ setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+export const DragContext = createContext<DragContextType | undefined>(
+ undefined,
+);
+
+function TldrawComponent({ id }: { id: string }) {
+ const [isDraggingOver, setIsDraggingOver] = useState<boolean>(false);
+ return (
+ <DragContext.Provider value={{ isDraggingOver, setIsDraggingOver }}>
+ <div className="h-[98vh]" onDragOver={() => setIsDraggingOver(true)}>
+ <Thinkpad id={id} />
+ </div>
+ </DragContext.Provider>
+ );
+}
+
+export const Thinkpad = memo(({ id }: { id: string }) => {
+ const [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
+ status: "loading",
+ });
+ useEffect(() => {
+ const fetchStore = async () => {
+ const store = await loadRemoteSnapshot(id);
+
+ setStoreWithStatus({
+ store: store,
+ status: "not-synced",
+ });
+ };
+
+ fetchStore();
+ }, []);
+
+ const handleMount = useCallback((editor: Editor) => {
+ (window as any).app = editor;
+ (window as any).editor = editor;
+ editor.registerExternalAssetHandler("url", createAssetFromUrl);
+ editor.registerExternalContentHandler("url", ({ url, point, sources }) => {
+ createEmbedsFromUrl({ url, point, sources, editor });
+ });
+ }, []);
+
+ setUserPreferences({ id: "supermemory", colorScheme: "dark" });
+
+
+ return (
+ <Tldraw
+ className="relative"
+ components={components}
+ store={storeWithStatus}
+ shapeUtils={[twitterCardUtil, textCardUtil]}
+ onMount={handleMount}
+ >
+ <DropZone />
+ <div className="absolute left-1/2 top-0 z-[1000000] flex -translate-x-1/2 gap-2 bg-[#2C3439] text-[#B3BCC5]">
+ <SaveStatus id={id} />
+ </div>
+ </Tldraw>
+ );
+});
+
+export default TldrawComponent;
diff --git a/apps/web/components/canvas/tldrawDrop.tsx b/apps/web/components/canvas/tldrawDrop.tsx
new file mode 100644
index 00000000..38f2d479
--- /dev/null
+++ b/apps/web/components/canvas/tldrawDrop.tsx
@@ -0,0 +1,67 @@
+import { handleExternalDroppedContent } from "@/lib/ExternalDroppedContent";
+import { BomttomLeftIcon, BomttomRightIcon, TopLeftIcon, TopRightIcon } from "@repo/ui/icons";
+import Image from "next/image";
+import { useContext } from "react";
+import { useEditor } from "tldraw";
+import { DragContext } from "./tldrawComponent";
+
+type CardData = {
+ type: any; // Adjust this according to your actual type
+ title: string;
+ content: string;
+ url: string;
+};
+
+function DropZone() {
+ const editor = useEditor();
+
+ const dragContext = useContext(DragContext);
+ if (!dragContext) {
+ throw new Error("Thinkpad must be used within a DragContextProvider");
+ }
+ const { isDraggingOver, setIsDraggingOver } = dragContext;
+
+ const handleDrop = (event: React.DragEvent<HTMLDivElement>) => {
+ event.preventDefault();
+ const data = event.dataTransfer.getData("application/json");
+ try {
+ const cardData: CardData = JSON.parse(data);
+ console.log("drop", cardData);
+ handleExternalDroppedContent({ editor, droppedData: cardData });
+ } catch (e) {
+ const textData = event.dataTransfer.getData("text/plain");
+ handleExternalDroppedContent({ editor, droppedData: textData });
+ }
+ setIsDraggingOver(false);
+ };
+
+ return (
+ <div
+ onDrop={handleDrop}
+ onDragOver={(e) => e.preventDefault()}
+ onDragLeave={() => setIsDraggingOver(false)}
+ className={`w-full absolute ${
+ isDraggingOver ? "z-[500]" : "z-[100] pointer-events-none"
+ } rounded-lg h-full flex items-center justify-center`}
+ >
+ {isDraggingOver && (
+ <>
+ <div className="absolute top-4 left-8">
+ <Image src={TopRightIcon} alt="" />
+ </div>
+ <div className="absolute top-4 right-8">
+ <Image src={TopLeftIcon} alt="" />
+ </div>
+ <div className="absolute bottom-4 left-8">
+ <Image src={BomttomLeftIcon} alt="" />
+ </div>
+ <div className="absolute bottom-4 right-8">
+ <Image src={BomttomRightIcon} alt="" />
+ </div>
+ </>
+ )}
+ </div>
+ );
+}
+
+export default DropZone; \ No newline at end of file