aboutsummaryrefslogtreecommitdiff
path: root/apps/web/lib
diff options
context:
space:
mode:
authorDhravya <[email protected]>2024-06-30 20:50:24 -0500
committerDhravya <[email protected]>2024-06-30 20:50:24 -0500
commitffd141ade4e6074ee486da7f74f31e3905807cb9 (patch)
tree505d73b0a7c04cdec93d7f5be88c635642716c15 /apps/web/lib
parentshow updates in the extension (diff)
parentMerge pull request #93 from Dhravya/editor (diff)
downloadsupermemory-ffd141ade4e6074ee486da7f74f31e3905807cb9.tar.xz
supermemory-ffd141ade4e6074ee486da7f74f31e3905807cb9.zip
merge conflicts
Diffstat (limited to 'apps/web/lib')
-rw-r--r--apps/web/lib/context.ts18
-rw-r--r--apps/web/lib/createAssetUrl.ts94
-rw-r--r--apps/web/lib/createEmbeds.ts236
-rw-r--r--apps/web/lib/loadSnap.ts14
4 files changed, 362 insertions, 0 deletions
diff --git a/apps/web/lib/context.ts b/apps/web/lib/context.ts
new file mode 100644
index 00000000..840c0d31
--- /dev/null
+++ b/apps/web/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;
diff --git a/apps/web/lib/createAssetUrl.ts b/apps/web/lib/createAssetUrl.ts
new file mode 100644
index 00000000..05c2baea
--- /dev/null
+++ b/apps/web/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/apps/web/lib/createEmbeds.ts b/apps/web/lib/createEmbeds.ts
new file mode 100644
index 00000000..b3a7fb52
--- /dev/null
+++ b/apps/web/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/apps/web/lib/loadSnap.ts b/apps/web/lib/loadSnap.ts
new file mode 100644
index 00000000..083603eb
--- /dev/null
+++ b/apps/web/lib/loadSnap.ts
@@ -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;
+}