mirror of
https://github.com/supermemoryai/supermemory.git
synced 2026-09-07 08:26:15 +00:00
done!!
This commit is contained in:
parent
3c91c188a4
commit
5def889576
8 changed files with 236 additions and 192 deletions
|
|
@ -6,6 +6,7 @@ import Sidepanel from "./sidepanel";
|
|||
import Image from "next/image";
|
||||
import { DragIcon } from "@repo/ui/icons";
|
||||
import { useRef, useState } from "react";
|
||||
import { ChevronRight } from "lucide-react";
|
||||
|
||||
export default function ResizableLayout({ id }: { id: string }) {
|
||||
const panelGroupRef = useRef(null);
|
||||
|
|
@ -19,7 +20,7 @@ export default function ResizableLayout({ id }: { id: string }) {
|
|||
|
||||
return (
|
||||
<PanelGroup
|
||||
className="text-white h-screen py-[1vh] px-[1vh] bg-[#111417]"
|
||||
className="text-white h-screen py-[1vh] px-[1vh] bg-[#171B1F]"
|
||||
direction="horizontal"
|
||||
ref={panelGroupRef}
|
||||
onLayout={(sizes) => {
|
||||
|
|
@ -31,9 +32,9 @@ export default function ResizableLayout({ id }: { id: string }) {
|
|||
</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`}
|
||||
className={`rounded-lg bg-[#2F363B] absolute top-1/2 z-[1000000] -translate-y-1/2 transition-all py-2 ${!isLeftPanelCollapsed && "px-1"}`}
|
||||
>
|
||||
<Image src={DragIcon} alt="drag-icon" />
|
||||
{isLeftPanelCollapsed ? <ChevronRight className="text-[#989EA4] h-6 w-4 cursor-pointer" /> : <Image src={DragIcon} alt="drag-icon" />}
|
||||
</div>
|
||||
</PanelResizeHandle>
|
||||
<Panel defaultSize={70} minSize={60}>
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import Link from "next/link";
|
|||
import Card from "./sidepanelcard";
|
||||
import { sourcesZod } from "@repo/shared-types";
|
||||
import { toast } from "sonner";
|
||||
import { MagnifyingGlassIcon } from "@heroicons/react/24/outline";
|
||||
|
||||
type card = {
|
||||
title: string;
|
||||
|
|
@ -17,7 +18,7 @@ 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">
|
||||
<div className="flex 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"
|
||||
|
|
@ -25,10 +26,6 @@ function Sidepanel() {
|
|||
<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">
|
||||
|
|
@ -47,6 +44,7 @@ function Sidepanel() {
|
|||
function Search({ setContent }: { setContent: (e: any) => void }) {
|
||||
return (
|
||||
<form
|
||||
className="flex justify-between items-center rounded-md bg-[#121718]"
|
||||
action={async (FormData) => {
|
||||
const search = FormData.get("search") as string;
|
||||
|
||||
|
|
@ -91,9 +89,12 @@ function Search({ setContent }: { setContent: (e: any) => void }) {
|
|||
<input
|
||||
name="search"
|
||||
placeholder="search memories..."
|
||||
className="rounded-md w-full bg-[#121718] p-3 text-lg outline-none"
|
||||
className="w-full bg-transparent p-3 text-lg outline-none flex-grow"
|
||||
type="text"
|
||||
/>
|
||||
<button type="submit" className=" border-l-2 border-gray-600 h-full p-3">
|
||||
<MagnifyingGlassIcon className=" h-5 text-[#fff]" />
|
||||
</button>
|
||||
</form>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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({
|
||||
|
|
@ -17,15 +18,32 @@ export default function Card({
|
|||
source?: string;
|
||||
}) {
|
||||
const [ref, bounds] = useMeasure();
|
||||
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
|
||||
const handleDragStart = (event: React.DragEvent<HTMLDivElement>) => {
|
||||
const handleDragStart = (event: React.DragEvent<HTMLDivElement>, dragSource: 'icon' | 'link' | 'parent') => {
|
||||
setIsDragging(true);
|
||||
event.dataTransfer.setData(
|
||||
"application/json",
|
||||
JSON.stringify({ type, title, content, source })
|
||||
);
|
||||
|
||||
switch(dragSource) {
|
||||
case 'icon':
|
||||
event.dataTransfer.setData("application/json", JSON.stringify({
|
||||
type,
|
||||
title,
|
||||
content,
|
||||
text: true
|
||||
}));
|
||||
break;
|
||||
case 'link':
|
||||
event.dataTransfer.setData("text/plain", source || '');
|
||||
break;
|
||||
case 'parent':
|
||||
event.dataTransfer.setData("application/json", JSON.stringify({
|
||||
type,
|
||||
title,
|
||||
content,
|
||||
text: false
|
||||
}));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragEnd = () => {
|
||||
|
|
@ -37,13 +55,21 @@ export default function Card({
|
|||
<div
|
||||
draggable
|
||||
onDragEnd={handleDragEnd}
|
||||
onDragStart={handleDragStart}
|
||||
onDragStart={(e) => handleDragStart(e, 'parent')}
|
||||
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"}`}>
|
||||
<a
|
||||
href={source}
|
||||
className={`${source && "cursor-pointer"}`}
|
||||
draggable
|
||||
onDragStart={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDragStart(e, 'icon');
|
||||
}}
|
||||
>
|
||||
<Icon type={type} />
|
||||
</a>
|
||||
<div>
|
||||
|
|
@ -51,6 +77,17 @@ export default function Card({
|
|||
<p className="group-hover:line-clamp-[12] transition-all line-clamp-3 text-gray-200">
|
||||
{content}
|
||||
</p>
|
||||
<a
|
||||
draggable
|
||||
href={source}
|
||||
className="line-clamp-[1] text-blue-500"
|
||||
onDragStart={(e) => {
|
||||
e.stopPropagation();
|
||||
handleDragStart(e, 'link');
|
||||
}}
|
||||
>
|
||||
{source}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -12,9 +12,8 @@ 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";
|
||||
import createEmbedsFromUrl from "@/lib/createEmbeds";
|
||||
|
||||
interface DragContextType {
|
||||
isDraggingOver: boolean;
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@ import { useEditor } from "tldraw";
|
|||
import { DragContext } from "./tldrawComponent";
|
||||
|
||||
type CardData = {
|
||||
type: any; // Adjust this according to your actual type
|
||||
title: string;
|
||||
type: string;
|
||||
content: string;
|
||||
url: string;
|
||||
text: boolean;
|
||||
};
|
||||
|
||||
function DropZone() {
|
||||
|
|
|
|||
|
|
@ -1,103 +1,5 @@
|
|||
import {
|
||||
AssetRecordType,
|
||||
Editor,
|
||||
TLAsset,
|
||||
TLAssetId,
|
||||
TLBookmarkShape,
|
||||
TLExternalContentSource,
|
||||
TLShapePartial,
|
||||
Vec,
|
||||
VecLike,
|
||||
createShapeId,
|
||||
getEmbedInfo,
|
||||
getHashForString,
|
||||
} from "tldraw";
|
||||
import { unfirlSite } from "@/app/actions/fetchers";
|
||||
|
||||
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);
|
||||
|
||||
const urlPattern = /https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/;
|
||||
if (urlPattern.test(url)) {
|
||||
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);
|
||||
|
||||
let asset = editor.getAsset(assetId) as TLAsset;
|
||||
let shouldAlsoCreateAsset = false;
|
||||
if (!asset) {
|
||||
shouldAlsoCreateAsset = true;
|
||||
try {
|
||||
const bookmarkAsset = await editor.getAssetForExternalContent({
|
||||
type: "url",
|
||||
url,
|
||||
});
|
||||
const value = await unfirlSite(url);
|
||||
if (bookmarkAsset) {
|
||||
if (bookmarkAsset.type === "bookmark" ){
|
||||
if (value.title ) bookmarkAsset.props.title = value.title;
|
||||
if (value.image) bookmarkAsset.props.image = value.image;
|
||||
if (value.description)
|
||||
bookmarkAsset.props.description = value.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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
import {Editor} from "tldraw";
|
||||
import createEmbedsFromUrl from "./createEmbeds";
|
||||
|
||||
function processURL(input: string): string | null {
|
||||
let str = input.trim();
|
||||
|
|
@ -116,6 +18,19 @@ function processURL(input: string): string | null {
|
|||
}
|
||||
}
|
||||
|
||||
function formatContent(title: string, content: string): string {
|
||||
const totalLength = title.length + content.length;
|
||||
const totalHeight = Math.ceil(totalLength * (2 / 3));
|
||||
const titleLines = Math.ceil(totalHeight * (2 / 5));
|
||||
const contentLines = totalHeight - titleLines;
|
||||
|
||||
const titleWithNewLines = title + '\n'.repeat(titleLines);
|
||||
const contentWithNewLines = content + '\n'.repeat(contentLines);
|
||||
|
||||
return `${titleWithNewLines.trim()}\n\n${contentWithNewLines.trim()}`;
|
||||
}
|
||||
|
||||
|
||||
function formatTextToRatio(text: string): { height: number; width: number } {
|
||||
const RATIO = 4 / 3;
|
||||
const FONT_SIZE = 15;
|
||||
|
|
@ -162,9 +77,8 @@ function formatTextToRatio(text: string): { height: number; width: number } {
|
|||
type CardData = {
|
||||
title: string;
|
||||
type: string;
|
||||
source: string;
|
||||
content: string;
|
||||
numChunks: string;
|
||||
text: boolean;
|
||||
};
|
||||
|
||||
type DroppedData = CardData | string | { imageUrl: string };
|
||||
|
|
@ -188,17 +102,20 @@ export function handleExternalDroppedContent({
|
|||
createTextCard(editor, position, droppedData, "String Content");
|
||||
}
|
||||
} else if ("imageUrl" in droppedData) {
|
||||
// Handle image URL (implementation not provided in original code)
|
||||
} else {
|
||||
const { content, title, source, type } = droppedData;
|
||||
const processedURL = processURL(source);
|
||||
// if (processedURL) {
|
||||
// createEmbedsFromUrl({ editor, url: processedURL });
|
||||
// } else {
|
||||
// }
|
||||
createTextCard(editor, position, title, type, content);
|
||||
}
|
||||
}
|
||||
const { content, title, type, text } = droppedData;
|
||||
if (text){
|
||||
const { height, width } = formatTextToRatio(content);
|
||||
editor.createShape({
|
||||
type: 'text',
|
||||
x: position.x - width / 2,
|
||||
y: position.y - height / 2,
|
||||
props: { text: formatContent(title, content) },
|
||||
})
|
||||
}else {
|
||||
createTextCard(editor, position, title, type, content);
|
||||
}
|
||||
}}
|
||||
|
||||
function createTextCard(editor: Editor, position: { x: number; y: number }, content: string, type: string, extraInfo: string = "") {
|
||||
const { height, width } = formatTextToRatio(content);
|
||||
|
|
@ -209,62 +126,3 @@ function createTextCard(editor: Editor, position: { x: number; y: number }, cont
|
|||
props: { content, type, extrainfo: extraInfo, h: 200, w: 400 },
|
||||
});
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
|||
88
apps/web/lib/createEmbeds.ts
Normal file
88
apps/web/lib/createEmbeds.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
import { AssetRecordType, Editor, TLAsset, TLAssetId, TLExternalContentSource, VecLike, getEmbedInfo, getHashForString } from "tldraw";
|
||||
import { createEmptyBookmarkShape } from "./drophelpers";
|
||||
import { unfirlSite } from "@/app/actions/fetchers";
|
||||
|
||||
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);
|
||||
|
||||
const urlPattern = /https?:\/\/(x\.com|twitter\.com)\/[\w]+\/[\w]+\/[\d]+/;
|
||||
if (urlPattern.test(url)) {
|
||||
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);
|
||||
|
||||
let asset = editor.getAsset(assetId) as TLAsset;
|
||||
let shouldAlsoCreateAsset = false;
|
||||
if (!asset) {
|
||||
shouldAlsoCreateAsset = true;
|
||||
try {
|
||||
const bookmarkAsset = await editor.getAssetForExternalContent({
|
||||
type: "url",
|
||||
url,
|
||||
});
|
||||
const value = await unfirlSite(url);
|
||||
if (bookmarkAsset) {
|
||||
if (bookmarkAsset.type === "bookmark" ){
|
||||
if (value.title ) bookmarkAsset.props.title = value.title;
|
||||
if (value.image) bookmarkAsset.props.image = value.image;
|
||||
if (value.description)
|
||||
bookmarkAsset.props.description = value.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,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
}
|
||||
60
apps/web/lib/drophelpers.ts
Normal file
60
apps/web/lib/drophelpers.ts
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
import { Editor, TLBookmarkShape, TLShapePartial, Vec, VecLike, createShapeId } from "tldraw";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue