aboutsummaryrefslogtreecommitdiff
path: root/packages/ui/components
diff options
context:
space:
mode:
authorKinfe Michael Tariku <[email protected]>2024-06-25 19:56:54 +0300
committerGitHub <[email protected]>2024-06-25 19:56:54 +0300
commitf46e42c2dfd1b223d4ad701a86d05fc0bb380e45 (patch)
treef17fdfadf3bec08eee7f02da33af952796657254 /packages/ui/components
parentfix: import using absolute path (diff)
parentdev and prod databases (diff)
downloadsupermemory-f46e42c2dfd1b223d4ad701a86d05fc0bb380e45.tar.xz
supermemory-f46e42c2dfd1b223d4ad701a86d05fc0bb380e45.zip
Merge branch 'main' into feat/landing_revamp
Diffstat (limited to 'packages/ui/components')
-rw-r--r--packages/ui/components/QueryInput.tsx60
-rw-r--r--packages/ui/components/canvas/components/canvas.tsx94
-rw-r--r--packages/ui/components/canvas/components/draggableComponent.tsx71
-rw-r--r--packages/ui/components/canvas/components/dropComponent.tsx203
-rw-r--r--packages/ui/components/canvas/components/enabledComp copy.tsx22
-rw-r--r--packages/ui/components/canvas/components/enabledComp.tsx22
-rw-r--r--packages/ui/components/canvas/components/savesnap.tsx43
-rw-r--r--packages/ui/components/canvas/components/textCard.tsx45
-rw-r--r--packages/ui/components/canvas/components/twitterCard.tsx84
-rw-r--r--packages/ui/components/canvas/lib/context.ts18
-rw-r--r--packages/ui/components/canvas/lib/createAssetUrl.ts94
-rw-r--r--packages/ui/components/canvas/lib/createEmbeds.ts236
-rw-r--r--packages/ui/components/canvas/lib/loadSnap.ts14
13 files changed, 1006 insertions, 0 deletions
diff --git a/packages/ui/components/QueryInput.tsx b/packages/ui/components/QueryInput.tsx
new file mode 100644
index 00000000..ba476dda
--- /dev/null
+++ b/packages/ui/components/QueryInput.tsx
@@ -0,0 +1,60 @@
+import React from "react";
+import Divider from "../shadcn/divider";
+import { ArrowRightIcon } from "../icons";
+import Image from "next/image";
+
+function QueryInput() {
+ return (
+ <div>
+ <div className="bg-secondary rounded-[20px] h-[68 px]">
+ {/* input and action button */}
+ <form className="flex gap-4 p-2.5">
+ <textarea
+ name="q"
+ cols={30}
+ rows={4}
+ className="bg-transparent h-12 focus:h-[128px] no-scrollbar pt-3 px-2 text-base placeholder:text-[#5D6165] text-[#9DA0A4] focus:text-white duration-200 tracking-[3%] outline-none resize-none w-full"
+ placeholder="Ask your second brain..."
+ // onKeyDown={(e) => {
+ // if (e.key === "Enter") {
+ // e.preventDefault();
+ // if (!e.shiftKey) push(parseQ());
+ // }
+ // }}
+ // onChange={(e) => setQ(e.target.value)}
+ // value={q}
+ // disabled={disabled}
+ />
+
+ <button
+ // type="submit"
+ // onClick={e => e.preventDefault()}
+ // disabled={disabled}
+ className="h-12 w-12 rounded-[14px] bg-[#21303D] all-center shrink-0 hover:brightness-125 duration-200 outline-none focus:outline focus:outline-primary active:scale-90"
+ >
+ <Image src={ArrowRightIcon} alt="Right arrow icon" />
+ </button>
+ </form>
+
+ {/* <Divider /> */}
+ </div>
+ {/* selected sources */}
+ {/* <div className="flex items-center gap-6 p-2 h-auto bg-secondary"> */}
+ {/* <MultipleSelector
+ key={options.length}
+ disabled={disabled}
+ defaultOptions={options}
+ onChange={(e) => setSelectedSpaces(e.map((x) => parseInt(x.value)))}
+ placeholder="Focus on specific spaces..."
+ emptyIndicator={
+ <p className="text-center text-lg leading-10 text-gray-600 dark:text-gray-400">
+ no results found.
+ </p>
+ }
+ /> */}
+ {/* </div> */}
+ </div>
+ );
+}
+
+export default QueryInput;
diff --git a/packages/ui/components/canvas/components/canvas.tsx b/packages/ui/components/canvas/components/canvas.tsx
new file mode 100644
index 00000000..57b63d49
--- /dev/null
+++ b/packages/ui/components/canvas/components/canvas.tsx
@@ -0,0 +1,94 @@
+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 "./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 [storeWithStatus, setStoreWithStatus] = useState<TLStoreWithStatus>({
+ status: "loading",
+ });
+ useEffect(() => {
+ const fetchStore = async () => {
+ const store = await loadRemoteSnapshot();
+
+ 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", isDarkMode: true });
+
+ 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 />
+ </div>
+ <DropZone />
+ </Tldraw>
+ </div>
+ );
+});
diff --git a/packages/ui/components/canvas/components/draggableComponent.tsx b/packages/ui/components/canvas/components/draggableComponent.tsx
new file mode 100644
index 00000000..d0832e81
--- /dev/null
+++ b/packages/ui/components/canvas/components/draggableComponent.tsx
@@ -0,0 +1,71 @@
+import Image from "next/image";
+import { useRef, useState } from "react";
+
+interface DraggableComponentsProps {
+ content: string;
+ extraInfo?: string;
+ icon: string;
+ iconAlt: string;
+}
+
+export default function DraggableComponentsContainer({
+ content,
+}: {
+ content: DraggableComponentsProps[];
+}) {
+ 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}
+ />
+ );
+ })}
+ </div>
+ );
+}
+
+function DraggableComponents({
+ content,
+ extraInfo,
+ icon,
+ iconAlt,
+}: DraggableComponentsProps) {
+ 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 (
+ <div
+ 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]"}`}
+ >
+ <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>
+ </div>
+ </div>
+ );
+}
diff --git a/packages/ui/components/canvas/components/dropComponent.tsx b/packages/ui/components/canvas/components/dropComponent.tsx
new file mode 100644
index 00000000..0374f367
--- /dev/null
+++ b/packages/ui/components/canvas/components/dropComponent.tsx
@@ -0,0 +1,203 @@
+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/packages/ui/components/canvas/components/enabledComp copy.tsx b/packages/ui/components/canvas/components/enabledComp copy.tsx
new file mode 100644
index 00000000..85811b82
--- /dev/null
+++ b/packages/ui/components/canvas/components/enabledComp copy.tsx
@@ -0,0 +1,22 @@
+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,
+}; \ No newline at end of file
diff --git a/packages/ui/components/canvas/components/enabledComp.tsx b/packages/ui/components/canvas/components/enabledComp.tsx
new file mode 100644
index 00000000..85811b82
--- /dev/null
+++ b/packages/ui/components/canvas/components/enabledComp.tsx
@@ -0,0 +1,22 @@
+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,
+}; \ No newline at end of file
diff --git a/packages/ui/components/canvas/components/savesnap.tsx b/packages/ui/components/canvas/components/savesnap.tsx
new file mode 100644
index 00000000..f82e97e3
--- /dev/null
+++ b/packages/ui/components/canvas/components/savesnap.tsx
@@ -0,0 +1,43 @@
+import { useCallback, useEffect, useState } from "react";
+import { debounce, useEditor } from "tldraw";
+
+export function SaveStatus() {
+ 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 res = await fetch(
+ "https://learning-cf.pruthvirajthinks.workers.dev/post/page3",
+ {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({
+ data: snapshot,
+ }),
+ },
+ );
+
+ console.log(await res.json());
+ setSave("saved!");
+ }, 3000),
+ [editor], // Dependency array ensures the function is not recreated on every render
+ );
+
+ useEffect(() => {
+ const unsubscribe = editor.store.listen(
+ () => {
+ setSave("saving...");
+ debouncedSave();
+ },
+ { scope: "document", source: "user" },
+ );
+
+ return () => unsubscribe(); // Cleanup on unmount
+ }, [editor, debouncedSave]);
+
+ return <button>{save}</button>;
+} \ No newline at end of file
diff --git a/packages/ui/components/canvas/components/textCard.tsx b/packages/ui/components/canvas/components/textCard.tsx
new file mode 100644
index 00000000..b24dae52
--- /dev/null
+++ b/packages/ui/components/canvas/components/textCard.tsx
@@ -0,0 +1,45 @@
+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: "#2C3439",
+ borderRadius: "16px",
+ padding: "8px 14px",
+ }}
+ >
+ <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/packages/ui/components/canvas/components/twitterCard.tsx b/packages/ui/components/canvas/components/twitterCard.tsx
new file mode 100644
index 00000000..c5582a98
--- /dev/null
+++ b/packages/ui/components/canvas/components/twitterCard.tsx
@@ -0,0 +1,84 @@
+import { BaseBoxShapeUtil, HTMLContainer, TLBaseShape, toDomPrecision } from "tldraw";
+
+type ITwitterCardShape = TLBaseShape<
+ "Twittercard",
+ { w: number; h: number; url: string }
+>;
+
+export class twitterCardUtil extends BaseBoxShapeUtil<ITwitterCardShape> {
+ static override type = "Twittercard" as const;
+
+ getDefaultProps(): ITwitterCardShape["props"] {
+ return {
+ w: 500,
+ h: 550,
+ url: "",
+ };
+ }
+
+ component(s: ITwitterCardShape) {
+ return (
+ <HTMLContainer className="flex h-full w-full items-center justify-center">
+ <TwitterPost
+ url={s.props.url}
+ width={s.props.w}
+ isInteractive={false}
+ height={s.props.h}
+ />
+ </HTMLContainer>
+ );
+ }
+
+ indicator(shape: ITwitterCardShape) {
+ return <rect width={shape.props.w} height={shape.props.h} />;
+ }
+}
+
+function TwitterPost({
+ isInteractive,
+ width,
+ height,
+ url,
+}: {
+ isInteractive: boolean;
+ width: number;
+ height: number;
+ url: string;
+}) {
+ const link = (() => {
+ try {
+ const urlObj = new URL(url);
+ const path = urlObj.pathname;
+ return path;
+ } catch (error) {
+ console.error("Invalid URL", error);
+ return null;
+ }
+ })();
+
+ return (
+ <iframe
+ className="tl-embed"
+ draggable={false}
+ width={toDomPrecision(width)}
+ height={toDomPrecision(height)}
+ seamless
+ referrerPolicy="no-referrer-when-downgrade"
+ style={{
+ pointerEvents: isInteractive ? "all" : "none",
+ zIndex: isInteractive ? "" : "-1",
+ }}
+ srcDoc={`
+ <html lang="en">
+ <head>
+ <meta charset="UTF-8">
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
+ <title>Document</title>
+ </head>
+ <body>
+ <blockquote data-theme="dark" class="twitter-tweet"><p lang="en" dir="ltr"><a href="https://twitter.com${link}"></a></blockquote> <script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>
+ </body>
+ </html>`}
+ />
+ );
+}
diff --git a/packages/ui/components/canvas/lib/context.ts b/packages/ui/components/canvas/lib/context.ts
new file mode 100644
index 00000000..4e6ecd1c
--- /dev/null
+++ b/packages/ui/components/canvas/lib/context.ts
@@ -0,0 +1,18 @@
+import { createContext, useContext } from 'react';
+
+export interface DragContextType {
+ isDraggingOver: boolean;
+ setIsDraggingOver: React.Dispatch<React.SetStateAction<boolean>>;
+}
+
+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');
+ }
+ return context;
+};
+
+export default DragContext; \ No newline at end of file
diff --git a/packages/ui/components/canvas/lib/createAssetUrl.ts b/packages/ui/components/canvas/lib/createAssetUrl.ts
new file mode 100644
index 00000000..05c2baea
--- /dev/null
+++ b/packages/ui/components/canvas/lib/createAssetUrl.ts
@@ -0,0 +1,94 @@
+import {
+ AssetRecordType,
+ TLAsset,
+ getHashForString,
+ truncateStringWithEllipsis,
+} from "tldraw";
+// import { BOOKMARK_ENDPOINT } from './config'
+
+interface ResponseBody {
+ title?: string;
+ description?: string;
+ image?: string;
+}
+
+export async function createAssetFromUrl({
+ url,
+}: {
+ type: "url";
+ url: string;
+}): Promise<TLAsset> {
+ // try {
+ // // First, try to get the meta data from our endpoint
+ // const meta = (await (
+ // await fetch(BOOKMARK_ENDPOINT, {
+ // method: 'POST',
+ // headers: {
+ // 'Content-Type': 'application/json',
+ // },
+ // body: JSON.stringify({
+ // url,
+ // }),
+ // })
+ // ).json()) as ResponseBody
+
+ // return {
+ // id: AssetRecordType.createId(getHashForString(url)),
+ // typeName: 'asset',
+ // type: 'bookmark',
+ // props: {
+ // src: url,
+ // description: meta.description ?? '',
+ // image: meta.image ?? '',
+ // title: meta.title ?? truncateStringWithEllipsis(url, 32),
+ // },
+ // meta: {},
+ // }
+ // } catch (error) {
+ // Otherwise, fallback to fetching data from the url
+
+ let meta: { image: string; title: string; description: string };
+
+ try {
+ const resp = await fetch(url, { method: "GET", mode: "no-cors" });
+ const html = await resp.text();
+ const doc = new DOMParser().parseFromString(html, "text/html");
+ meta = {
+ image:
+ doc.head
+ .querySelector('meta[property="og:image"]')
+ ?.getAttribute("content") ?? "",
+ title:
+ doc.head
+ .querySelector('meta[property="og:title"]')
+ ?.getAttribute("content") ?? truncateStringWithEllipsis(url, 32),
+ description:
+ doc.head
+ .querySelector('meta[property="og:description"]')
+ ?.getAttribute("content") ?? "",
+ };
+ } catch (error) {
+ console.error(error);
+ meta = {
+ image: "",
+ title: truncateStringWithEllipsis(url, 32),
+ description: "",
+ };
+ }
+
+ // Create the bookmark asset from the meta
+ return {
+ id: AssetRecordType.createId(getHashForString(url)),
+ typeName: "asset",
+ type: "bookmark",
+ props: {
+ src: url,
+ image: meta.image,
+ title: meta.title,
+ description: meta.description,
+ favicon: meta.image,
+ },
+ meta: {},
+ };
+ // }
+}
diff --git a/packages/ui/components/canvas/lib/createEmbeds.ts b/packages/ui/components/canvas/lib/createEmbeds.ts
new file mode 100644
index 00000000..b3a7fb52
--- /dev/null
+++ b/packages/ui/components/canvas/lib/createEmbeds.ts
@@ -0,0 +1,236 @@
+// @ts-nocheck TODO: A LOT OF TS ERRORS HERE
+
+import {
+ AssetRecordType,
+ Editor,
+ TLAsset,
+ TLAssetId,
+ TLBookmarkShape,
+ TLExternalContentSource,
+ TLShapePartial,
+ Vec,
+ VecLike,
+ createShapeId,
+ getEmbedInfo,
+ getHashForString,
+} from "tldraw";
+
+export default async function createEmbedsFromUrl({
+ url,
+ point,
+ sources,
+ editor,
+}: {
+ url: string;
+ point?: VecLike | undefined;
+ sources?: TLExternalContentSource[] | undefined;
+ editor: Editor;
+}) {
+ const position =
+ point ??
+ (editor.inputs.shiftKey
+ ? editor.inputs.currentPagePoint
+ : editor.getViewportPageBounds().center);
+
+ if (url?.includes("x.com") || url?.includes("twitter.com")) {
+ return editor.createShape({
+ type: "Twittercard",
+ x: position.x - 250,
+ y: position.y - 150,
+ props: { url: url },
+ });
+ }
+
+ // try to paste as an embed first
+ const embedInfo = getEmbedInfo(url);
+
+ if (embedInfo) {
+ return editor.putExternalContent({
+ type: "embed",
+ url: embedInfo.url,
+ point,
+ embed: embedInfo.definition,
+ });
+ }
+
+ const assetId: TLAssetId = AssetRecordType.createId(getHashForString(url));
+ const shape = createEmptyBookmarkShape(editor, url, position);
+
+ // Use an existing asset if we have one, or else else create a new one
+ let asset = editor.getAsset(assetId) as TLAsset;
+ let shouldAlsoCreateAsset = false;
+ if (!asset) {
+ shouldAlsoCreateAsset = true;
+ try {
+ const bookmarkAsset = await editor.getAssetForExternalContent({
+ type: "url",
+ url,
+ });
+ const fetchWebsite: {
+ title?: string;
+ image?: string;
+ description?: string;
+ } = await (
+ await fetch(`/api/unfirlsite?website=${url}`, {
+ method: "POST",
+ })
+ ).json();
+ if (bookmarkAsset) {
+ if (fetchWebsite.title) bookmarkAsset.props.title = fetchWebsite.title;
+ if (fetchWebsite.image) bookmarkAsset.props.image = fetchWebsite.image;
+ if (fetchWebsite.description)
+ bookmarkAsset.props.description = fetchWebsite.description;
+ }
+ if (!bookmarkAsset) throw Error("Could not create an asset");
+ asset = bookmarkAsset;
+ } catch (e) {
+ console.log(e);
+ return;
+ }
+ }
+
+ editor.batch(() => {
+ if (shouldAlsoCreateAsset) {
+ editor.createAssets([asset]);
+ }
+
+ editor.updateShapes([
+ {
+ id: shape.id,
+ type: shape.type,
+ props: {
+ assetId: asset.id,
+ },
+ },
+ ]);
+ });
+}
+
+function isURL(str: string) {
+ try {
+ new URL(str);
+ return true;
+ } catch {
+ return false;
+ }
+}
+
+function formatTextToRatio(text: string) {
+ const totalWidth = text.length;
+ const maxLineWidth = Math.floor(totalWidth / 10);
+
+ const words = text.split(" ");
+ let lines = [];
+ let currentLine = "";
+
+ words.forEach((word) => {
+ if ((currentLine + word).length <= maxLineWidth) {
+ currentLine += (currentLine ? " " : "") + word;
+ } else {
+ lines.push(currentLine);
+ currentLine = word;
+ }
+ });
+ if (currentLine) {
+ lines.push(currentLine);
+ }
+ return { height: (lines.length + 1) * 18, width: maxLineWidth * 10 };
+}
+
+export function handleExternalDroppedContent({
+ text,
+ editor,
+}: {
+ text: string;
+ editor: Editor;
+}) {
+ const position = editor.inputs.shiftKey
+ ? editor.inputs.currentPagePoint
+ : editor.getViewportPageBounds().center;
+
+ if (isURL(text)) {
+ createEmbedsFromUrl({ editor, url: text });
+ } else {
+ // editor.createShape({
+ // type: "text",
+ // x: position.x - 75,
+ // y: position.y - 75,
+ // props: {
+ // text: text,
+ // size: "s",
+ // textAlign: "start",
+ // },
+ // });
+ const { height, width } = formatTextToRatio(text);
+ editor.createShape({
+ type: "Textcard",
+ x: position.x - width / 2,
+ y: position.y - height / 2,
+ props: {
+ content: text,
+ extrainfo: "https://chatgpt.com/c/762cd44e-1752-495b-967a-aa3c23c6024a",
+ w: width,
+ h: height,
+ },
+ });
+ }
+}
+
+function centerSelectionAroundPoint(editor: Editor, position: VecLike) {
+ // Re-position shapes so that the center of the group is at the provided point
+ const viewportPageBounds = editor.getViewportPageBounds();
+ let selectionPageBounds = editor.getSelectionPageBounds();
+
+ if (selectionPageBounds) {
+ const offset = selectionPageBounds!.center.sub(position);
+
+ editor.updateShapes(
+ editor.getSelectedShapes().map((shape) => {
+ const localRotation = editor
+ .getShapeParentTransform(shape)
+ .decompose().rotation;
+ const localDelta = Vec.Rot(offset, -localRotation);
+ return {
+ id: shape.id,
+ type: shape.type,
+ x: shape.x! - localDelta.x,
+ y: shape.y! - localDelta.y,
+ };
+ }),
+ );
+ }
+
+ // Zoom out to fit the shapes, if necessary
+ selectionPageBounds = editor.getSelectionPageBounds();
+ if (
+ selectionPageBounds &&
+ !viewportPageBounds.contains(selectionPageBounds)
+ ) {
+ editor.zoomToSelection();
+ }
+}
+
+export function createEmptyBookmarkShape(
+ editor: Editor,
+ url: string,
+ position: VecLike,
+): TLBookmarkShape {
+ const partial: TLShapePartial = {
+ id: createShapeId(),
+ type: "bookmark",
+ x: position.x - 150,
+ y: position.y - 160,
+ opacity: 1,
+ props: {
+ assetId: null,
+ url,
+ },
+ };
+
+ editor.batch(() => {
+ editor.createShapes([partial]).select(partial.id);
+ centerSelectionAroundPoint(editor, position);
+ });
+
+ return editor.getShape(partial.id) as TLBookmarkShape;
+}
diff --git a/packages/ui/components/canvas/lib/loadSnap.ts b/packages/ui/components/canvas/lib/loadSnap.ts
new file mode 100644
index 00000000..846b1967
--- /dev/null
+++ b/packages/ui/components/canvas/lib/loadSnap.ts
@@ -0,0 +1,14 @@
+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;
+}