Commit 82da20e1 authored by Steven's avatar Steven

feat: implement graph of relations

parent 952428c1
...@@ -34,6 +34,7 @@ ...@@ -34,6 +34,7 @@
"mermaid": "^11.2.1", "mermaid": "^11.2.1",
"react": "^18.3.1", "react": "^18.3.1",
"react-dom": "^18.3.1", "react-dom": "^18.3.1",
"react-force-graph-2d": "^1.25.6",
"react-hot-toast": "^2.4.1", "react-hot-toast": "^2.4.1",
"react-i18next": "^15.0.2", "react-i18next": "^15.0.2",
"react-leaflet": "^4.2.1", "react-leaflet": "^4.2.1",
......
This diff is collapsed.
...@@ -3,6 +3,7 @@ import { isEqual } from "lodash-es"; ...@@ -3,6 +3,7 @@ import { isEqual } from "lodash-es";
import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon } from "lucide-react"; import { CheckCircleIcon, Code2Icon, HashIcon, LinkIcon } from "lucide-react";
import { Memo, MemoProperty } from "@/types/proto/api/v1/memo_service"; import { Memo, MemoProperty } from "@/types/proto/api/v1/memo_service";
import { useTranslate } from "@/utils/i18n"; import { useTranslate } from "@/utils/i18n";
import MemoRelationForceGraph from "../MemoRelationForceGraph";
interface Props { interface Props {
memo: Memo; memo: Memo;
...@@ -21,7 +22,13 @@ const MemoDetailSidebar = ({ memo, className }: Props) => { ...@@ -21,7 +22,13 @@ const MemoDetailSidebar = ({ memo, className }: Props) => {
className, 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"> <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"> <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> <span>Created at</span>
......
...@@ -27,7 +27,7 @@ const AddMemoRelationPopover = (props: Props) => { ...@@ -27,7 +27,7 @@ const AddMemoRelationPopover = (props: Props) => {
const [isFetching, setIsFetching] = useState<boolean>(true); const [isFetching, setIsFetching] = useState<boolean>(true);
const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]); const [fetchedMemos, setFetchedMemos] = useState<Memo[]>([]);
const [selectedMemos, setSelectedMemos] = 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 [popoverOpen, setPopoverOpen] = useState<boolean>(false);
const filteredMemos = fetchedMemos.filter( const filteredMemos = fetchedMemos.filter(
......
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;
import MemoRelationForceGraph from "./MemoRelationForceGraph";
export * from "./utils";
export default MemoRelationForceGraph;
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>> {}
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,
};
};
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment