feat: implement graph of relations

pull/4046/head
Steven 5 months ago
parent 952428c15d
commit 82da20e1c3

@ -34,6 +34,7 @@
"mermaid": "^11.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-force-graph-2d": "^1.25.6",
"react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.2",
"react-leaflet": "^4.2.1",

@ -83,6 +83,9 @@ importers:
react-dom:
specifier: ^18.3.1
version: 18.3.1(react@18.3.1)
react-force-graph-2d:
specifier: ^1.25.6
version: 1.25.6(react@18.3.1)
react-hot-toast:
specifier: ^2.4.1
version: 2.4.1(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -1170,6 +1173,9 @@ packages:
'@vue/compiler-sfc':
optional: true
'@tweenjs/tween.js@25.0.0':
resolution: {integrity: sha512-XKLA6syeBUaPzx4j3qwMqzzq+V4uo72BnlbOjmuljLrRqdsd3qnzvZZoxvMHZ23ndsRS4aufU6JOZYpCbU6T1A==}
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@ -1402,6 +1408,10 @@ packages:
abort-controller-x@0.4.3:
resolution: {integrity: sha512-VtUwTNU8fpMwvWGn4xE93ywbogTYsuT+AUxAXOeelbXuQVIwNmC5YLeho9sH4vZ4ITW8414TTAOG1nW6uIVHCA==}
accessor-fn@1.5.1:
resolution: {integrity: sha512-zZpFYBqIL1Aqg+f2qmYHJ8+yIZF7/tP6PUGx2/QM0uGPSO5UegpinmkNwDohxWtOj586BpMPVRUjce2HI6xB3A==}
engines: {node: '>=12'}
acorn-jsx@5.3.2:
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies:
@ -1502,6 +1512,9 @@ packages:
balanced-match@1.0.2:
resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==}
bezier-js@6.1.4:
resolution: {integrity: sha512-PA0FW9ZpcHbojUCMu28z9Vg/fNkwTj5YhusSAjHHDfHDGLxJ6YUKrAN2vk1fP2MMOxVw4Oko16FMlRGVBGqLKg==}
binary-extensions@2.3.0:
resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==}
engines: {node: '>=8'}
@ -1536,6 +1549,10 @@ packages:
caniuse-lite@1.0.30001662:
resolution: {integrity: sha512-sgMUVwLmGseH8ZIrm1d51UbrhqMCH3jvS7gF/M6byuHOnKyLOBL7W8yz5V02OHwgLGA36o/AFhWzzh4uc5aqTA==}
canvas-color-tracker@1.3.1:
resolution: {integrity: sha512-eNycxGS7oQ3IS/9QQY41f/aQjiO9Y/MtedhCgSdsbLSxC9EyUD8L3ehl/Q3Kfmvt8um79S45PBV+5Rxm5ztdSw==}
engines: {node: '>=12'}
chalk@2.4.2:
resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==}
engines: {node: '>=4'}
@ -1664,6 +1681,9 @@ packages:
resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==}
engines: {node: '>=12'}
d3-binarytree@1.0.2:
resolution: {integrity: sha512-cElUNH+sHu95L04m92pG73t2MEJXKu+GeKUN1TJkFsu93E5W8E9Sc3kHEGJKgenGvj19m6upSn2EunvMgMD2Yw==}
d3-brush@3.0.0:
resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==}
engines: {node: '>=12'}
@ -1705,6 +1725,10 @@ packages:
resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==}
engines: {node: '>=12'}
d3-force-3d@3.0.5:
resolution: {integrity: sha512-tdwhAhoTYZY/a6eo9nR7HP3xSW/C6XvJTbeRpR92nlPzH6OiE+4MliN9feuSFd0tPtEUo+191qOhCTWx3NYifg==}
engines: {node: '>=12'}
d3-force@3.0.0:
resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==}
engines: {node: '>=12'}
@ -1725,6 +1749,9 @@ packages:
resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==}
engines: {node: '>=12'}
d3-octree@1.0.2:
resolution: {integrity: sha512-Qxg4oirJrNXauiuC94uKMbgxwnhdda9xRLl9ihq45srlJ4Ga3CSgqGcAL8iW7N5CIv4Oz8x3E734ulxyvHPvwA==}
d3-path@1.0.9:
resolution: {integrity: sha512-VLaYcn81dtHVTjEHd8B+pbe9yHWpXKZUC87PzoFmsFrJqgFwDe/qxfp5MlfsfM1V5E/iVt0MmEbWQ7FVIXh/bg==}
@ -2036,6 +2063,10 @@ packages:
for-each@0.3.3:
resolution: {integrity: sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==}
force-graph@1.44.2:
resolution: {integrity: sha512-SthMFx+CdvMM4kwN/KoqmNx1//H5smwukOgQi7VsyqxDQU72vFE6/1/TDqb5NG8UnZjjaKEMcusOnAkpV+4zNA==}
engines: {node: '>=12'}
foreground-child@3.3.0:
resolution: {integrity: sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==}
engines: {node: '>=14'}
@ -2043,6 +2074,9 @@ packages:
fraction.js@4.3.7:
resolution: {integrity: sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==}
fromentries@1.3.2:
resolution: {integrity: sha512-cHEpEQHUg0f8XdtZCc2ZAhrHzKzT0MrFUTcvx+hfxYu7rGMDc5SKoXFh+n4YigxsHXRzc6OrCshdR1bWH6HHyg==}
fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==}
@ -2200,6 +2234,10 @@ packages:
resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==}
engines: {node: '>=0.8.19'}
index-array-by@1.4.2:
resolution: {integrity: sha512-SP23P27OUKzXWEC/TOyWlwLviofQkCSCKONnc62eItjp69yCZZPqDQtr3Pw5gJDnPeUMqExmKydNZaJO0FU9pw==}
engines: {node: '>=12'}
inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
@ -2359,6 +2397,10 @@ packages:
javascript-natural-sort@0.7.1:
resolution: {integrity: sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==}
jerrypick@1.1.1:
resolution: {integrity: sha512-XTtedPYEyVp4t6hJrXuRKr/jHj8SC4z+4K0b396PMkov6muL+i8IIamJIvZWe3jUspgIJak0P+BaWKawMYNBLg==}
engines: {node: '>=12'}
jiti@1.21.6:
resolution: {integrity: sha512-2yTgeWTWzMWkHu6Jp9NKgePDaYHbntiwvYuuJLbbN9vl7DC9DvXKOB2BC3ZZ92D3cvV/aflH0osDfwpHepQ53w==}
hasBin: true
@ -2402,6 +2444,10 @@ packages:
resolution: {integrity: sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==}
engines: {node: '>=4.0'}
kapsule@1.14.6:
resolution: {integrity: sha512-wSi6tHNOfXrIK2Pvv6BhZ9ukzhbp+XZlOOPWSVGUbqfFsnnli4Eq8FN6TaWJv2e17sY5+fKYVxa4DP2oPGlKhg==}
engines: {node: '>=12'}
katex@0.16.11:
resolution: {integrity: sha512-RQrI8rlHY92OLf3rho/Ts8i/XvjgguEjOkO1BEXcU3N8BqPpSzBNwV/G0Ukr+P/l3ivvJUE/Fa/CwbS6HesGNQ==}
hasBin: true
@ -2769,6 +2815,12 @@ packages:
peerDependencies:
react: ^18.3.1
react-force-graph-2d@1.25.6:
resolution: {integrity: sha512-qFarXF1pazVtGUoJul7cecqVGdfLoMQiX/98mewULM9YXDXesDfVExBaLkoz9KHN/guWdoSfH2sTUHQA9hpmDw==}
engines: {node: '>=12'}
peerDependencies:
react: '*'
react-hot-toast@2.4.1:
resolution: {integrity: sha512-j8z+cQbWIM5LY37pR6uZR6D4LfseplqnuAO4co4u8917hBUvXlEqyP1ZzqVLcqoyUesZZv/ImreoCeHVDpE5pQ==}
engines: {node: '>=10'}
@ -2795,6 +2847,12 @@ packages:
react-is@18.3.1:
resolution: {integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==}
react-kapsule@2.4.1:
resolution: {integrity: sha512-dAPhdUHZWbCH46lF2wHSqtJ3bdmNCYiV10v7QBgzYGAVMup/SZt6WHgP0PzURQvinVRKpq6PmsO4af1W37mQdg==}
engines: {node: '>=12'}
peerDependencies:
react: '>=16.13.1'
react-leaflet@4.2.1:
resolution: {integrity: sha512-p9chkvhcKrWn/H/1FFeVSqLdReGwn2qmiobOQGO3BifX+/vV/39qhY8dGqbdcPh1e6jxh/QHriLXr7a4eLFK4Q==}
peerDependencies:
@ -3139,6 +3197,9 @@ packages:
resolution: {integrity: sha512-dTEWWNu6JmeVXY0ZYoPuH5cRIwc0MeGbJwah9KUNYSJwommQpCzTySTpEe8Gs1J23aeWEuAobe4Ag7EHVt/LOg==}
engines: {node: '>=10'}
tinycolor2@1.6.0:
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
tinyexec@0.3.0:
resolution: {integrity: sha512-tVGE0mVJPGb0chKhqmsoosjsS+qUnJVGJpZgsHYQcGoPlG3B51R3PouqTgEGH2Dc9jjFyOqOpix6ZHNMXp1FZg==}
@ -4290,6 +4351,8 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@tweenjs/tween.js@25.0.0': {}
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.25.6
@ -4575,6 +4638,8 @@ snapshots:
abort-controller-x@0.4.3: {}
accessor-fn@1.5.1: {}
acorn-jsx@5.3.2(acorn@8.12.1):
dependencies:
acorn: 8.12.1
@ -4697,6 +4762,8 @@ snapshots:
balanced-match@1.0.2: {}
bezier-js@6.1.4: {}
binary-extensions@2.3.0: {}
brace-expansion@1.1.11:
@ -4733,6 +4800,10 @@ snapshots:
caniuse-lite@1.0.30001662: {}
canvas-color-tracker@1.3.1:
dependencies:
tinycolor2: 1.6.0
chalk@2.4.2:
dependencies:
ansi-styles: 3.2.1
@ -4869,6 +4940,8 @@ snapshots:
d3-axis@3.0.0: {}
d3-binarytree@1.0.2: {}
d3-brush@3.0.0:
dependencies:
d3-dispatch: 3.0.1
@ -4910,6 +4983,14 @@ snapshots:
dependencies:
d3-dsv: 3.0.1
d3-force-3d@3.0.5:
dependencies:
d3-binarytree: 1.0.2
d3-dispatch: 3.0.1
d3-octree: 1.0.2
d3-quadtree: 3.0.1
d3-timer: 3.0.1
d3-force@3.0.0:
dependencies:
d3-dispatch: 3.0.1
@ -4928,6 +5009,8 @@ snapshots:
dependencies:
d3-color: 3.1.0
d3-octree@1.0.2: {}
d3-path@1.0.9: {}
d3-path@3.1.0: {}
@ -5392,6 +5475,23 @@ snapshots:
dependencies:
is-callable: 1.2.7
force-graph@1.44.2:
dependencies:
'@tweenjs/tween.js': 25.0.0
accessor-fn: 1.5.1
bezier-js: 6.1.4
canvas-color-tracker: 1.3.1
d3-array: 3.2.4
d3-drag: 3.0.0
d3-force-3d: 3.0.5
d3-scale: 4.0.2
d3-scale-chromatic: 3.1.0
d3-selection: 3.0.0
d3-zoom: 3.0.0
index-array-by: 1.4.2
kapsule: 1.14.6
lodash-es: 4.17.21
foreground-child@3.3.0:
dependencies:
cross-spawn: 7.0.3
@ -5399,6 +5499,8 @@ snapshots:
fraction.js@4.3.7: {}
fromentries@1.3.2: {}
fs.realpath@1.0.0: {}
fsevents@2.3.3:
@ -5552,6 +5654,8 @@ snapshots:
imurmurhash@0.1.4: {}
index-array-by@1.4.2: {}
inflight@1.0.6:
dependencies:
once: 1.4.0
@ -5703,6 +5807,8 @@ snapshots:
javascript-natural-sort@0.7.1: {}
jerrypick@1.1.1: {}
jiti@1.21.6: {}
js-base64@3.7.7: {}
@ -5734,6 +5840,10 @@ snapshots:
object.assign: 4.1.5
object.values: 1.2.0
kapsule@1.14.6:
dependencies:
lodash-es: 4.17.21
katex@0.16.11:
dependencies:
commander: 8.3.0
@ -6123,6 +6233,13 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-force-graph-2d@1.25.6(react@18.3.1):
dependencies:
force-graph: 1.44.2
prop-types: 15.8.1
react: 18.3.1
react-kapsule: 2.4.1(react@18.3.1)
react-hot-toast@2.4.1(csstype@3.1.3)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
goober: 2.1.14(csstype@3.1.3)
@ -6144,6 +6261,12 @@ snapshots:
react-is@18.3.1: {}
react-kapsule@2.4.1(react@18.3.1):
dependencies:
fromentries: 1.3.2
jerrypick: 1.1.1
react: 18.3.1
react-leaflet@4.2.1(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@react-leaflet/core': 2.1.0(leaflet@1.9.4)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
@ -6556,6 +6679,8 @@ snapshots:
throttle-debounce@3.0.1: {}
tinycolor2@1.6.0: {}
tinyexec@0.3.0: {}
to-fast-properties@2.0.0: {}

@ -3,6 +3,7 @@ import { isEqual } from "lodash-es";
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon } from "lucide-react";
import { Memo, MemoProperty } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n";
import MemoRelationForceGraph from "../MemoRelationForceGraph";
interface Props {
memo: Memo;
@ -21,7 +22,13 @@ const MemoDetailSidebar = ({ memo, className }: Props) => {
className,
)}
>
<div className="flex flex-col justify-start items-start w-full mt-1 px-1 gap-2 h-auto shrink-0 flex-nowrap hide-scrollbar">
<div className="flex flex-col justify-start items-start w-full px-1 gap-2 h-auto shrink-0 flex-nowrap hide-scrollbar">
{memo.relations.length > 0 && (
<div className="relative w-full h-36 border rounded-lg bg-zinc-50 dark:bg-zinc-900 dark:border-zinc-800">
<MemoRelationForceGraph className="w-full h-full" memo={memo} />
<span className="absolute top-1 left-2 text-xs opacity-60 font-mono">Relations</span>
</div>
)}
<div className="w-full flex flex-col">
<p className="flex flex-row justify-start items-center w-full gap-1 mb-1 text-sm leading-6 text-gray-400 dark:text-gray-500 select-none">
<span>Created at</span>

@ -27,7 +27,7 @@ const AddMemoRelationPopover = (props: Props) => {
const [isFetching, setIsFetching] = useState<boolean>(true);
const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]);
const [selectedMemos, setSelectedMemos] = useState<Memo[]>([]);
const [embedded, setEmbedded] = useState<boolean>(true);
const [embedded, setEmbedded] = useState<boolean>(false);
const [popoverOpen, setPopoverOpen] = useState<boolean>(false);
const filteredMemos = fetchedMemos.filter(

@ -0,0 +1,50 @@
import { useColorScheme } from "@mui/joy";
import clsx from "clsx";
import { useEffect, useRef, useState } from "react";
import ForceGraph2D from "react-force-graph-2d";
import { Memo } from "@/types/proto/api/v1/memo_service";
import { FGMethods } from "./types";
import { convertMemoRelationsToGraphData } from "./utils";
interface Props {
memo: Memo;
className?: string;
}
const MAIN_NODE_COLOR = "#14b8a6";
const DEFAULT_NODE_COLOR = "#a1a1aa";
const MemoRelationForceGraph = ({ className, memo }: Props) => {
const { mode } = useColorScheme();
const containerRef = useRef<HTMLDivElement>(null);
const graphRef = useRef<FGMethods | undefined>(undefined);
const [graphSize, setGraphSize] = useState({ width: 0, height: 0 });
useEffect(() => {
if (!containerRef.current) return;
setGraphSize(containerRef.current.getBoundingClientRect());
}, []);
const onNodeClick = () => {
// TODO: Handle node click event
};
return (
<div ref={containerRef} className={clsx("dark:opacity-80", className)}>
<ForceGraph2D
ref={graphRef}
width={graphSize.width}
height={graphSize.height}
enableZoomInteraction
cooldownTicks={0}
nodeColor={(node) => (node.name === memo.name ? MAIN_NODE_COLOR : DEFAULT_NODE_COLOR)}
nodeRelSize={3}
linkColor={() => (mode === "light" ? "" : "#525252")}
graphData={convertMemoRelationsToGraphData(memo.relations)}
onNodeClick={onNodeClick}
/>
</div>
);
};
export default MemoRelationForceGraph;

@ -0,0 +1,5 @@
import MemoRelationForceGraph from "./MemoRelationForceGraph";
export * from "./utils";
export default MemoRelationForceGraph;

@ -0,0 +1,11 @@
import { ForceGraphMethods, LinkObject, NodeObject } from "react-force-graph-2d";
export interface NodeType {
name: string;
}
export interface LinkType {
// ...add more additional properties relevant to the link here.
}
export interface FGMethods extends ForceGraphMethods<NodeObject<NodeType>, LinkObject<NodeType, LinkType>> {}

@ -0,0 +1,35 @@
import { GraphData, LinkObject, NodeObject } from "react-force-graph-2d";
import { MemoRelation } from "@/types/proto/api/v1/memo_relation_service";
import { LinkType, NodeType } from "./types";
export const convertMemoRelationsToGraphData = (memoRelations: MemoRelation[]): GraphData<NodeType, LinkType> => {
const nodesMap = new Map<string, NodeObject<NodeType>>();
const links: LinkObject<NodeType, LinkType>[] = [];
// Iterate through memoRelations to populate nodes and links.
memoRelations.forEach((relation) => {
const { memo, relatedMemo, type } = relation;
// Add memo node if not already present.
if (!nodesMap.has(memo)) {
nodesMap.set(memo, { id: memo, name: memo });
}
// Add related_memo node if not already present.
if (!nodesMap.has(relatedMemo)) {
nodesMap.set(relatedMemo, { id: relatedMemo, name: relatedMemo });
}
// Create link between memo and relatedMemo.
links.push({
source: memo,
target: relatedMemo,
type, // Include the type of relation as a property of the link.
});
});
return {
nodes: Array.from(nodesMap.values()),
links,
};
};
Loading…
Cancel
Save