From e2b9b377c6a5bc25f8a1433f0856309ee260c085 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 9 Aug 2025 01:40:50 +0530 Subject: [PATCH 01/44] settings for context and page groups --- .../components/settings/PageGroupPanel.tsx | 196 ++++++++++++++++++ .../roam/src/components/settings/Settings.tsx | 7 + .../settings/SuggestiveModeSettings.tsx | 53 +++++ apps/roam/src/utils/configPageTabs.ts | 28 +++ apps/roam/src/utils/discourseConfigRef.ts | 45 ++++ 5 files changed, 329 insertions(+) create mode 100644 apps/roam/src/components/settings/PageGroupPanel.tsx create mode 100644 apps/roam/src/components/settings/SuggestiveModeSettings.tsx diff --git a/apps/roam/src/components/settings/PageGroupPanel.tsx b/apps/roam/src/components/settings/PageGroupPanel.tsx new file mode 100644 index 000000000..99db30c42 --- /dev/null +++ b/apps/roam/src/components/settings/PageGroupPanel.tsx @@ -0,0 +1,196 @@ +import React, { useState, useCallback, useMemo } from "react"; +import { Label, Button, Intent, Tag } from "@blueprintjs/core"; +import Description from "roamjs-components/components/Description"; +import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; +import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; +import createBlock from "roamjs-components/writes/createBlock"; +import deleteBlock from "roamjs-components/writes/deleteBlock"; +import getDiscourseNodes from "~/utils/getDiscourseNodes"; +import getAllPageNames from "roamjs-components/queries/getAllPageNames"; + +type PageGroupData = { + uid: string; + name: string; + pages: { uid: string; name: string }[]; +}; + +const PageGroupsPanel = ({ uid }: { uid: string }) => { + const [pageGroups, setPageGroups] = useState(() => + getBasicTreeByParentUid(uid).map((node) => ({ + uid: node.uid, + name: node.text, + pages: node.children.map((c) => ({ uid: c.uid, name: c.text })), + })), + ); + + const refreshGroups = useCallback(() => { + setPageGroups( + getBasicTreeByParentUid(uid).map((node) => ({ + uid: node.uid, + name: node.text, + pages: node.children.map((c) => ({ uid: c.uid, name: c.text })), + })), + ); + }, [uid, setPageGroups]); + + const [newGroupName, setNewGroupName] = useState(""); + const [newPageInputs, setNewPageInputs] = useState>( + {}, + ); + const [autocompleteKeys, setAutocompleteKeys] = useState< + Record + >({}); + + const addGroup = async (name: string) => { + if (!name || pageGroups.some((g) => g.name === name)) return; + await createBlock({ parentUid: uid, node: { text: name } }).then(() => { + refreshGroups(); + setNewGroupName(""); + }); + }; + + const removeGroup = async (groupUid: string) => { + await deleteBlock(groupUid).then(() => { + refreshGroups(); + }); + }; + + const addPageToGroup = async (groupUid: string, page: string) => { + const group = pageGroups.find((g) => g.uid === groupUid); + if (!page || group?.pages.some((p) => p.name === page)) { + return; + } + await createBlock({ parentUid: groupUid, node: { text: page } }).then( + () => { + refreshGroups(); + setNewPageInputs((prev) => ({ + ...prev, + [groupUid]: "", + })); + setAutocompleteKeys((prev) => ({ + ...prev, + [groupUid]: (prev[groupUid] || 0) + 1, + })); + }, + ); + }; + + const removePageFromGroup = async (pageUid: string) => { + await deleteBlock(pageUid).then(() => { + refreshGroups(); + }); + }; + + const getPageInput = (groupUid: string) => newPageInputs[groupUid] || ""; + const setPageInput = useCallback((groupUid: string, value: string) => { + setTimeout(() => { + setNewPageInputs((prev) => ({ + ...prev, + [groupUid]: value, + })); + }, 0); + }, []); + const getAutocompleteKey = (groupUid: string) => + autocompleteKeys[groupUid] || 0; + + return ( +
+ +
+ ); +}; + +export default PageGroupsPanel; diff --git a/apps/roam/src/components/settings/Settings.tsx b/apps/roam/src/components/settings/Settings.tsx index 6046fa158..ed5e89533 100644 --- a/apps/roam/src/components/settings/Settings.tsx +++ b/apps/roam/src/components/settings/Settings.tsx @@ -25,6 +25,7 @@ import sendErrorEmail from "~/utils/sendErrorEmail"; import HomePersonalSettings from "./HomePersonalSettings"; import refreshConfigTree from "~/utils/refreshConfigTree"; import { FeedbackWidget } from "~/components/BirdEatsBugs"; +import SuggestiveModeSettings from "./SuggestiveModeSettings"; type SectionHeaderProps = { children: React.ReactNode; @@ -160,6 +161,12 @@ export const SettingsDialog = ({ className="overflow-y-auto" panel={} /> + } + /> Grammar { + const settings = useMemo(() => { + refreshConfigTree(); + return getFormattedConfigTree(); + }, []); + + return ( +
+
+ Discourse Suggestions +
+
+
+
+ + +
+
+ +
+
+ ); +}; + +export default SuggestiveModeSettings; diff --git a/apps/roam/src/utils/configPageTabs.ts b/apps/roam/src/utils/configPageTabs.ts index 0fdf8a4d6..3112a6a63 100644 --- a/apps/roam/src/utils/configPageTabs.ts +++ b/apps/roam/src/utils/configPageTabs.ts @@ -14,6 +14,7 @@ import { CustomField, SelectField, } from "roamjs-components/components/ConfigPanels/types"; +import PageGroupsPanel from "~/components/settings/PageGroupPanel"; export const configPageTabs = (args: OnloadArgs): ConfigTab[] => [ { @@ -116,4 +117,31 @@ export const configPageTabs = (args: OnloadArgs): ConfigTab[] => [ }, ], }, + { + id: "suggestive-mode", + fields: [ + { + title: "Include Current Page Relations", + // @ts-ignore + Panel: FlagPanel, + description: + "Include relations from pages referenced on the current page", + }, + { + title: "Include Parent and Child Blocks", + // @ts-ignore + Panel: FlagPanel, + description: "Include relations from parent and child blocks", + }, + { + title: "Page Groups", + // @ts-ignore + Panel: CustomPanel, + description: "Set page groups to use for discourse suggestions", + options: { + component: PageGroupsPanel, + }, + }, + ], + }, ]; diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index 3cfc91cdc..7c4a38920 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -4,15 +4,56 @@ import { StringSetting, ExportConfigWithUids, getUidAndStringSetting, + BooleanSetting, + getUidAndBooleanSetting, } from "./getExportSettings"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import { getSubTree } from "roamjs-components/util"; const configTreeRef: { tree: RoamBasicNode[]; nodes: { [uid: string]: { text: string; children: RoamBasicNode[] } }; } = { tree: [], nodes: {} }; +type SuggestiveModeConfigWithUids = { + parentUid: string; + grabFromReferencedPages: BooleanSetting; + grabParentAndChildren: BooleanSetting; + pageGroups: { + uid: string; + name: string; + pages: { uid: string; name: string }[]; + }; +}; + +const getSuggestiveModeConfigAndUids = (): SuggestiveModeConfigWithUids => { + const suggestiveModeNode = getSubTree({ + tree: configTreeRef.tree, + key: "suggestive-mode", + }); + const pageGroupsNode = suggestiveModeNode.children.find( + (node) => node.text === "Page Groups", + ); + + return { + parentUid: suggestiveModeNode.uid, + grabFromReferencedPages: getUidAndBooleanSetting({ + tree: suggestiveModeNode.children, + text: "Include Current Page Relations", + }), + grabParentAndChildren: getUidAndBooleanSetting({ + tree: suggestiveModeNode.children, + text: "Include Parent and Child Blocks", + }), + pageGroups: { + uid: pageGroupsNode?.uid || "", + name: "page groups", + pages: [], + }, + }; +}; + type FormattedConfigTree = { settingsUid: string; grammarUid: string; @@ -21,13 +62,16 @@ type FormattedConfigTree = { trigger: StringSetting; export: ExportConfigWithUids; canvasPageFormat: StringSetting; + suggestiveMode: SuggestiveModeConfigWithUids; }; export const getFormattedConfigTree = (): FormattedConfigTree => { const settingsUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); + console.log("settingsUid", settingsUid); const grammarNode = configTreeRef.tree.find( (node) => node.text === "grammar", ); + console.log("grammarNode", grammarNode); const relationsNode = grammarNode?.children.find( (node) => node.text === "relations", ); @@ -47,6 +91,7 @@ export const getFormattedConfigTree = (): FormattedConfigTree => { tree: configTreeRef.tree, text: "Canvas Page Format", }), + suggestiveMode: getSuggestiveModeConfigAndUids(), }; }; export default configTreeRef; From da54c72b65b412e296d4185131c5b929443a17ea Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 10 Aug 2025 03:10:03 +0530 Subject: [PATCH 02/44] node settings --- .../settings/DiscourseNodeSuggestiveRules.tsx | 159 ++++++++++++++++++ .../src/components/settings/NodeConfig.tsx | 17 ++ apps/roam/src/utils/discourseConfigRef.ts | 2 - apps/roam/src/utils/renderNodeConfigPage.ts | 13 ++ 4 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx new file mode 100644 index 000000000..a4eb4adbd --- /dev/null +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -0,0 +1,159 @@ +import React, { + useState, + useMemo, + useEffect, + useRef, + useCallback, +} from "react"; +import { Button, Intent, Tooltip, Position } from "@blueprintjs/core"; +import BlocksPanel from "roamjs-components/components/ConfigPanels/BlocksPanel"; +import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; +import refreshConfigTree from "~/utils/refreshConfigTree"; +import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; +import { getUidAndBooleanSetting } from "~/utils/getExportSettings"; +import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; +import getSubTree from "roamjs-components/util/getSubTree"; + +type DiscourseNode = { + type: string; + text: string; + template?: string; + uid: string; +}; + +const BlockRenderer = ({ uid }: { uid: string }) => { + const containerRef = useRef(null); + + useEffect(() => { + const container = containerRef.current; + if (container) { + while (container.firstChild) { + container.removeChild(container.firstChild); + } + + if (uid) { + window.roamAlphaAPI.ui.components.renderBlock({ + uid: uid, + el: container, + }); + } + } + }, [uid]); + + return
; +}; + +const DiscourseNodeSuggestiveRules = ({ + node, + parentUid, +}: { + node: DiscourseNode; + parentUid: string; +}) => { + const nodeUid = node.type; + const nodeConfigTree = useMemo(() => { + refreshConfigTree(); + const tree = getBasicTreeByParentUid(parentUid); + const embeddingRefNode = tree.find((n) => + n.text.startsWith("Embedding Block Ref"), + ); + const match = embeddingRefNode?.children?.[0]?.text?.match(/\(\((.*)\)\)/); + const blockRef = match ? `((${match[1]}))` : ""; + return { + embeddingRef: blockRef, + embeddingRefUid: embeddingRefNode?.uid || "", + isFirstChild: getUidAndBooleanSetting({ + tree: tree, + text: "First Child", + }), + }; + }, [parentUid]); + + const [embeddingRef, setEmbeddingRef] = useState(nodeConfigTree.embeddingRef); + + useEffect(() => { + if (nodeConfigTree.embeddingRef !== embeddingRef) { + setEmbeddingRef(nodeConfigTree.embeddingRef); + } + }, [nodeConfigTree.embeddingRef]); + + const blockUidToRender = useMemo(() => { + const match = embeddingRef?.match(/\(\((.*)\)\)/); + return match ? match[1] : ""; + }, [embeddingRef]); + + const templateUid = useMemo( + () => + getSubTree({ + parentUid: nodeUid, + key: "Template", + }).uid, + [nodeUid], + ); + + const handleEmbeddingRefChange = useCallback( + (e: React.ChangeEvent) => { + const newValue = e.target.value; + setEmbeddingRef(newValue); + }, + [], + ); + + return ( +
+ + + + + {blockUidToRender && ( +
+
Preview:
+ +
+ )} + + + +
+ +
+
+ ); +}; + +export default DiscourseNodeSuggestiveRules; diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 0b1bd5eca..8f49869e0 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -15,6 +15,7 @@ import { OnloadArgs } from "roamjs-components/types"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import createBlock from "roamjs-components/writes/createBlock"; import updateBlock from "roamjs-components/writes/updateBlock"; +import DiscourseNodeSuggestiveRules from "./DiscourseNodeSuggestiveRules"; const ValidatedInputPanel = ({ label, @@ -327,6 +328,22 @@ const NodeConfig = ({
} /> + + n.text === "Suggestive Rules", + )?.uid || "" + } + /> + + } + /> ); diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index 7c4a38920..4bdb4e784 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -67,11 +67,9 @@ type FormattedConfigTree = { export const getFormattedConfigTree = (): FormattedConfigTree => { const settingsUid = getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE); - console.log("settingsUid", settingsUid); const grammarNode = configTreeRef.tree.find( (node) => node.text === "grammar", ); - console.log("grammarNode", grammarNode); const relationsNode = grammarNode?.children.find( (node) => node.text === "relations", ); diff --git a/apps/roam/src/utils/renderNodeConfigPage.ts b/apps/roam/src/utils/renderNodeConfigPage.ts index de2ee07b6..0e5d223dc 100644 --- a/apps/roam/src/utils/renderNodeConfigPage.ts +++ b/apps/roam/src/utils/renderNodeConfigPage.ts @@ -24,6 +24,7 @@ import { } from "~/components"; import getDiscourseNodes from "~/utils/getDiscourseNodes"; import { render as configPageRender } from "roamjs-components/components/ConfigPage"; +import DiscourseNodeSuggestiveRules from "~/components/settings/DiscourseNodeSuggestiveRules"; export const DISCOURSE_CONFIG_PAGE_TITLE = "roam/js/discourse-graph"; export const NODE_CONFIG_PAGE_TITLE = "discourse-graph/nodes/"; @@ -147,6 +148,18 @@ export const renderNodeConfigPage = ({ description: `Whether to color the node in the graph overview based on canvas color. This is based on the node's plain title as described by a \`has title\` condition in its specification.`, defaultValue: true, } as FieldPanel, + // @ts-ignore + { + title: "Suggestive Rules", + Panel: CustomPanel, + options: { + component: ({ uid }) => + React.createElement(DiscourseNodeSuggestiveRules, { + node, + parentUid: uid, + }), + }, + } as Field, ], }); From f83a0079e53c38f06b2d43f0cf2612a1eb1ee59e Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 10 Aug 2025 11:38:02 +0530 Subject: [PATCH 03/44] address review --- .../settings/DiscourseNodeSuggestiveRules.tsx | 2 +- .../src/components/settings/NodeConfig.tsx | 7 ++- .../components/settings/PageGroupPanel.tsx | 58 +++++++++++-------- .../settings/SuggestiveModeSettings.tsx | 7 ++- apps/roam/src/utils/configPageTabs.ts | 4 +- apps/roam/src/utils/discourseConfigRef.ts | 7 ++- 6 files changed, 48 insertions(+), 37 deletions(-) diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index a4eb4adbd..d75f4604d 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -105,7 +105,7 @@ const DiscourseNodeSuggestiveRules = ({ title="Template" description={`The template that auto fills ${node.text} page when generated.`} order={0} - parentUid={node.uid} + parentUid={nodeUid} uid={templateUid} defaultValue={node.template ? [{ text: node.template }] : undefined} /> diff --git a/apps/roam/src/components/settings/NodeConfig.tsx b/apps/roam/src/components/settings/NodeConfig.tsx index 8f49869e0..0261bb3b5 100644 --- a/apps/roam/src/components/settings/NodeConfig.tsx +++ b/apps/roam/src/components/settings/NodeConfig.tsx @@ -336,9 +336,10 @@ const NodeConfig = ({ n.text === "Suggestive Rules", - )?.uid || "" + getSubTree({ + parentUid: node.type, + key: "Suggestive Rules", + }).uid } /> diff --git a/apps/roam/src/components/settings/PageGroupPanel.tsx b/apps/roam/src/components/settings/PageGroupPanel.tsx index 99db30c42..38b20311e 100644 --- a/apps/roam/src/components/settings/PageGroupPanel.tsx +++ b/apps/roam/src/components/settings/PageGroupPanel.tsx @@ -43,16 +43,22 @@ const PageGroupsPanel = ({ uid }: { uid: string }) => { const addGroup = async (name: string) => { if (!name || pageGroups.some((g) => g.name === name)) return; - await createBlock({ parentUid: uid, node: { text: name } }).then(() => { + try { + await createBlock({ parentUid: uid, node: { text: name } }); refreshGroups(); setNewGroupName(""); - }); + } catch (e) { + console.error("Error adding group", e); + } }; const removeGroup = async (groupUid: string) => { - await deleteBlock(groupUid).then(() => { + try { + await deleteBlock(groupUid); refreshGroups(); - }); + } catch (e) { + console.error("Error removing group", e); + } }; const addPageToGroup = async (groupUid: string, page: string) => { @@ -60,35 +66,37 @@ const PageGroupsPanel = ({ uid }: { uid: string }) => { if (!page || group?.pages.some((p) => p.name === page)) { return; } - await createBlock({ parentUid: groupUid, node: { text: page } }).then( - () => { - refreshGroups(); - setNewPageInputs((prev) => ({ - ...prev, - [groupUid]: "", - })); - setAutocompleteKeys((prev) => ({ - ...prev, - [groupUid]: (prev[groupUid] || 0) + 1, - })); - }, - ); + try { + await createBlock({ parentUid: groupUid, node: { text: page } }); + refreshGroups(); + setNewPageInputs((prev) => ({ + ...prev, + [groupUid]: "", + })); + setAutocompleteKeys((prev) => ({ + ...prev, + [groupUid]: (prev[groupUid] || 0) + 1, + })); + } catch (e) { + console.error("Error adding page to group", e); + } }; const removePageFromGroup = async (pageUid: string) => { - await deleteBlock(pageUid).then(() => { + try { + await deleteBlock(pageUid); refreshGroups(); - }); + } catch (e) { + console.error("Error removing page from group", e); + } }; const getPageInput = (groupUid: string) => newPageInputs[groupUid] || ""; const setPageInput = useCallback((groupUid: string, value: string) => { - setTimeout(() => { - setNewPageInputs((prev) => ({ - ...prev, - [groupUid]: value, - })); - }, 0); + setNewPageInputs((prev) => ({ + ...prev, + [groupUid]: value, + })); }, []); const getAutocompleteKey = (groupUid: string) => autocompleteKeys[groupUid] || 0; diff --git a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx index 0371b48f1..3d2bc3316 100644 --- a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx +++ b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from "react"; +import React, { useEffect, useState } from "react"; import { Button, Intent } from "@blueprintjs/core"; import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; import refreshConfigTree from "~/utils/refreshConfigTree"; @@ -6,9 +6,10 @@ import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import PageGroupsPanel from "./PageGroupPanel"; const SuggestiveModeSettings = () => { - const settings = useMemo(() => { + const [settings, setSettings] = useState(() => getFormattedConfigTree()); + useEffect(() => { refreshConfigTree(); - return getFormattedConfigTree(); + setSettings(getFormattedConfigTree()); }, []); return ( diff --git a/apps/roam/src/utils/configPageTabs.ts b/apps/roam/src/utils/configPageTabs.ts index 3112a6a63..c2fe5a75a 100644 --- a/apps/roam/src/utils/configPageTabs.ts +++ b/apps/roam/src/utils/configPageTabs.ts @@ -133,15 +133,15 @@ export const configPageTabs = (args: OnloadArgs): ConfigTab[] => [ Panel: FlagPanel, description: "Include relations from parent and child blocks", }, + // @ts-ignore { title: "Page Groups", - // @ts-ignore Panel: CustomPanel, description: "Set page groups to use for discourse suggestions", options: { component: PageGroupsPanel, }, - }, + } as Field, ], }, ]; diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index 4bdb4e784..25239b9f1 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -32,9 +32,10 @@ const getSuggestiveModeConfigAndUids = (): SuggestiveModeConfigWithUids => { tree: configTreeRef.tree, key: "suggestive-mode", }); - const pageGroupsNode = suggestiveModeNode.children.find( - (node) => node.text === "Page Groups", - ); + const pageGroupsNode = getSubTree({ + parentUid: suggestiveModeNode.uid, + key: "page groups", + }); return { parentUid: suggestiveModeNode.uid, From ee58296419ef1c03c50498dfc5a3f319e858f1a0 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 10 Aug 2025 13:12:35 +0530 Subject: [PATCH 04/44] fix node creation if it does not exist --- .../components/settings/PageGroupPanel.tsx | 10 ++++---- .../settings/SuggestiveModeSettings.tsx | 24 +++++++++++++++---- apps/roam/src/utils/configPageTabs.ts | 2 +- apps/roam/src/utils/discourseConfigRef.ts | 2 +- 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/apps/roam/src/components/settings/PageGroupPanel.tsx b/apps/roam/src/components/settings/PageGroupPanel.tsx index 38b20311e..a9b928f03 100644 --- a/apps/roam/src/components/settings/PageGroupPanel.tsx +++ b/apps/roam/src/components/settings/PageGroupPanel.tsx @@ -93,10 +93,12 @@ const PageGroupsPanel = ({ uid }: { uid: string }) => { const getPageInput = (groupUid: string) => newPageInputs[groupUid] || ""; const setPageInput = useCallback((groupUid: string, value: string) => { - setNewPageInputs((prev) => ({ - ...prev, - [groupUid]: value, - })); + setTimeout(() => { + setNewPageInputs((prev) => ({ + ...prev, + [groupUid]: value, + })); + }, 0); }, []); const getAutocompleteKey = (groupUid: string) => autocompleteKeys[groupUid] || 0; diff --git a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx index 3d2bc3316..bb7097035 100644 --- a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx +++ b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx @@ -4,13 +4,29 @@ import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; import refreshConfigTree from "~/utils/refreshConfigTree"; import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import PageGroupsPanel from "./PageGroupPanel"; +import createBlock from "roamjs-components/writes/createBlock"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; const SuggestiveModeSettings = () => { - const [settings, setSettings] = useState(() => getFormattedConfigTree()); - useEffect(() => { + const [settings, setSettings] = useState(() => { refreshConfigTree(); - setSettings(getFormattedConfigTree()); - }, []); + return getFormattedConfigTree(); + }); + + useEffect(() => { + const ensureSettings = async () => { + if (!settings.suggestiveMode.parentUid) { + await createBlock({ + parentUid: getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE), + node: { text: "Suggestive mode" }, + }); + refreshConfigTree(); + setSettings(getFormattedConfigTree()); + } + }; + ensureSettings(); + }, [settings.suggestiveMode.parentUid]); return (
diff --git a/apps/roam/src/utils/configPageTabs.ts b/apps/roam/src/utils/configPageTabs.ts index c2fe5a75a..28aebf28a 100644 --- a/apps/roam/src/utils/configPageTabs.ts +++ b/apps/roam/src/utils/configPageTabs.ts @@ -118,7 +118,7 @@ export const configPageTabs = (args: OnloadArgs): ConfigTab[] => [ ], }, { - id: "suggestive-mode", + id: "Suggestive mode", fields: [ { title: "Include Current Page Relations", diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index 25239b9f1..9f11e2a38 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -30,7 +30,7 @@ type SuggestiveModeConfigWithUids = { const getSuggestiveModeConfigAndUids = (): SuggestiveModeConfigWithUids => { const suggestiveModeNode = getSubTree({ tree: configTreeRef.tree, - key: "suggestive-mode", + key: "Suggestive mode", }); const pageGroupsNode = getSubTree({ parentUid: suggestiveModeNode.uid, From 87f38d30a2abc24da93d9f3c1d4b768509cac945 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 10 Aug 2025 13:28:13 +0530 Subject: [PATCH 05/44] remove unused, refactor --- .../settings/DiscourseNodeSuggestiveRules.tsx | 8 +--- .../components/settings/PageGroupPanel.tsx | 3 +- apps/roam/src/utils/discourseConfigRef.ts | 46 ++----------------- .../utils/getSuggestiveModeConfigSettings.ts | 43 +++++++++++++++++ 4 files changed, 49 insertions(+), 51 deletions(-) create mode 100644 apps/roam/src/utils/getSuggestiveModeConfigSettings.ts diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index d75f4604d..b3b03cf47 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -13,13 +13,7 @@ import FlagPanel from "roamjs-components/components/ConfigPanels/FlagPanel"; import { getUidAndBooleanSetting } from "~/utils/getExportSettings"; import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; import getSubTree from "roamjs-components/util/getSubTree"; - -type DiscourseNode = { - type: string; - text: string; - template?: string; - uid: string; -}; +import { DiscourseNode } from "~/utils/getDiscourseNodes"; const BlockRenderer = ({ uid }: { uid: string }) => { const containerRef = useRef(null); diff --git a/apps/roam/src/components/settings/PageGroupPanel.tsx b/apps/roam/src/components/settings/PageGroupPanel.tsx index a9b928f03..11fa99769 100644 --- a/apps/roam/src/components/settings/PageGroupPanel.tsx +++ b/apps/roam/src/components/settings/PageGroupPanel.tsx @@ -1,11 +1,10 @@ -import React, { useState, useCallback, useMemo } from "react"; +import React, { useState, useCallback } from "react"; import { Label, Button, Intent, Tag } from "@blueprintjs/core"; import Description from "roamjs-components/components/Description"; import AutocompleteInput from "roamjs-components/components/AutocompleteInput"; import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import createBlock from "roamjs-components/writes/createBlock"; import deleteBlock from "roamjs-components/writes/deleteBlock"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; import getAllPageNames from "roamjs-components/queries/getAllPageNames"; type PageGroupData = { diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index 9f11e2a38..bc4041bcd 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -4,57 +4,19 @@ import { StringSetting, ExportConfigWithUids, getUidAndStringSetting, - BooleanSetting, - getUidAndBooleanSetting, } from "./getExportSettings"; import { DISCOURSE_CONFIG_PAGE_TITLE } from "~/utils/renderNodeConfigPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; -import { getSubTree } from "roamjs-components/util"; +import { + getSuggestiveModeConfigAndUids, + SuggestiveModeConfigWithUids, +} from "./getSuggestiveModeConfigSettings"; const configTreeRef: { tree: RoamBasicNode[]; nodes: { [uid: string]: { text: string; children: RoamBasicNode[] } }; } = { tree: [], nodes: {} }; -type SuggestiveModeConfigWithUids = { - parentUid: string; - grabFromReferencedPages: BooleanSetting; - grabParentAndChildren: BooleanSetting; - pageGroups: { - uid: string; - name: string; - pages: { uid: string; name: string }[]; - }; -}; - -const getSuggestiveModeConfigAndUids = (): SuggestiveModeConfigWithUids => { - const suggestiveModeNode = getSubTree({ - tree: configTreeRef.tree, - key: "Suggestive mode", - }); - const pageGroupsNode = getSubTree({ - parentUid: suggestiveModeNode.uid, - key: "page groups", - }); - - return { - parentUid: suggestiveModeNode.uid, - grabFromReferencedPages: getUidAndBooleanSetting({ - tree: suggestiveModeNode.children, - text: "Include Current Page Relations", - }), - grabParentAndChildren: getUidAndBooleanSetting({ - tree: suggestiveModeNode.children, - text: "Include Parent and Child Blocks", - }), - pageGroups: { - uid: pageGroupsNode?.uid || "", - name: "page groups", - pages: [], - }, - }; -}; - type FormattedConfigTree = { settingsUid: string; grammarUid: string; diff --git a/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts b/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts new file mode 100644 index 000000000..16a58b7a0 --- /dev/null +++ b/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts @@ -0,0 +1,43 @@ +import { getSubTree } from "roamjs-components/util"; +import configTreeRef from "./discourseConfigRef"; +import { BooleanSetting, getUidAndBooleanSetting } from "./getExportSettings"; + +export type SuggestiveModeConfigWithUids = { + parentUid: string; + grabFromReferencedPages: BooleanSetting; + grabParentAndChildren: BooleanSetting; + pageGroups: { + uid: string; + name: string; + pages: { uid: string; name: string }[]; + }; +}; + +export const getSuggestiveModeConfigAndUids = + (): SuggestiveModeConfigWithUids => { + const suggestiveModeNode = getSubTree({ + tree: configTreeRef.tree, + key: "Suggestive mode", + }); + const pageGroupsNode = getSubTree({ + parentUid: suggestiveModeNode.uid, + key: "page groups", + }); + + return { + parentUid: suggestiveModeNode.uid, + grabFromReferencedPages: getUidAndBooleanSetting({ + tree: suggestiveModeNode.children, + text: "Include Current Page Relations", + }), + grabParentAndChildren: getUidAndBooleanSetting({ + tree: suggestiveModeNode.children, + text: "Include Parent and Child Blocks", + }), + pageGroups: { + uid: pageGroupsNode?.uid || "", + name: "page groups", + pages: [], + }, + }; + }; From 5a6bfddfbf4dda00af7871c888aca8d6c08a5f88 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sun, 10 Aug 2025 19:07:55 +0530 Subject: [PATCH 06/44] address coderabbit review --- .../settings/DiscourseNodeSuggestiveRules.tsx | 42 +++++++++++-------- apps/roam/src/utils/configPageTabs.ts | 2 +- apps/roam/src/utils/discourseConfigRef.ts | 2 +- .../utils/getSuggestiveModeConfigSettings.ts | 12 +++--- 4 files changed, 33 insertions(+), 25 deletions(-) diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index b3b03cf47..23e948bcb 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -15,6 +15,23 @@ import TextPanel from "roamjs-components/components/ConfigPanels/TextPanel"; import getSubTree from "roamjs-components/util/getSubTree"; import { DiscourseNode } from "~/utils/getDiscourseNodes"; +const getNodeConfig = (parentUid: string) => { + const tree = getBasicTreeByParentUid(parentUid); + const embeddingRefNode = tree.find((n) => + n.text.startsWith("Embedding Block Ref"), + ); + const match = embeddingRefNode?.children?.[0]?.text?.match(/\(\((.*)\)\)/); + const blockRef = match ? `((${match[1]}))` : ""; + return { + embeddingRef: blockRef, + embeddingRefUid: embeddingRefNode?.uid || "", + isFirstChild: getUidAndBooleanSetting({ + tree: tree, + text: "First Child", + }), + }; +}; + const BlockRenderer = ({ uid }: { uid: string }) => { const containerRef = useRef(null); @@ -45,22 +62,13 @@ const DiscourseNodeSuggestiveRules = ({ parentUid: string; }) => { const nodeUid = node.type; - const nodeConfigTree = useMemo(() => { + const [nodeConfigTree, setNodeConfigTree] = useState(() => + getNodeConfig(parentUid), + ); + + useEffect(() => { refreshConfigTree(); - const tree = getBasicTreeByParentUid(parentUid); - const embeddingRefNode = tree.find((n) => - n.text.startsWith("Embedding Block Ref"), - ); - const match = embeddingRefNode?.children?.[0]?.text?.match(/\(\((.*)\)\)/); - const blockRef = match ? `((${match[1]}))` : ""; - return { - embeddingRef: blockRef, - embeddingRefUid: embeddingRefNode?.uid || "", - isFirstChild: getUidAndBooleanSetting({ - tree: tree, - text: "First Child", - }), - }; + setNodeConfigTree(getNodeConfig(parentUid)); }, [parentUid]); const [embeddingRef, setEmbeddingRef] = useState(nodeConfigTree.embeddingRef); @@ -101,7 +109,7 @@ const DiscourseNodeSuggestiveRules = ({ order={0} parentUid={nodeUid} uid={templateUid} - defaultValue={node.template ? [{ text: node.template }] : undefined} + defaultValue={node.template} />
diff --git a/apps/roam/src/utils/configPageTabs.ts b/apps/roam/src/utils/configPageTabs.ts index 28aebf28a..cb7d8aff1 100644 --- a/apps/roam/src/utils/configPageTabs.ts +++ b/apps/roam/src/utils/configPageTabs.ts @@ -128,7 +128,7 @@ export const configPageTabs = (args: OnloadArgs): ConfigTab[] => [ "Include relations from pages referenced on the current page", }, { - title: "Include Parent and Child Blocks", + title: "Include Parent And Child Blocks", // @ts-ignore Panel: FlagPanel, description: "Include relations from parent and child blocks", diff --git a/apps/roam/src/utils/discourseConfigRef.ts b/apps/roam/src/utils/discourseConfigRef.ts index bc4041bcd..48b2c1a1c 100644 --- a/apps/roam/src/utils/discourseConfigRef.ts +++ b/apps/roam/src/utils/discourseConfigRef.ts @@ -52,7 +52,7 @@ export const getFormattedConfigTree = (): FormattedConfigTree => { tree: configTreeRef.tree, text: "Canvas Page Format", }), - suggestiveMode: getSuggestiveModeConfigAndUids(), + suggestiveMode: getSuggestiveModeConfigAndUids(configTreeRef.tree), }; }; export default configTreeRef; diff --git a/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts b/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts index 16a58b7a0..a405c9a52 100644 --- a/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts +++ b/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts @@ -1,6 +1,6 @@ import { getSubTree } from "roamjs-components/util"; -import configTreeRef from "./discourseConfigRef"; import { BooleanSetting, getUidAndBooleanSetting } from "./getExportSettings"; +import { RoamBasicNode } from "roamjs-components/types"; export type SuggestiveModeConfigWithUids = { parentUid: string; @@ -14,14 +14,14 @@ export type SuggestiveModeConfigWithUids = { }; export const getSuggestiveModeConfigAndUids = - (): SuggestiveModeConfigWithUids => { + (tree: RoamBasicNode[]): SuggestiveModeConfigWithUids => { const suggestiveModeNode = getSubTree({ - tree: configTreeRef.tree, + tree, key: "Suggestive mode", }); const pageGroupsNode = getSubTree({ parentUid: suggestiveModeNode.uid, - key: "page groups", + key: "Page Groups", }); return { @@ -32,11 +32,11 @@ export const getSuggestiveModeConfigAndUids = }), grabParentAndChildren: getUidAndBooleanSetting({ tree: suggestiveModeNode.children, - text: "Include Parent and Child Blocks", + text: "Include Parent And Child Blocks", }), pageGroups: { uid: pageGroupsNode?.uid || "", - name: "page groups", + name: "Page Groups", pages: [], }, }; From 9815656da9d28a57434ae6b7b70537741fafb252 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 11 Aug 2025 12:59:22 +0530 Subject: [PATCH 07/44] unify spelling, handle fallback --- .../settings/DiscourseNodeSuggestiveRules.tsx | 2 +- .../settings/SuggestiveModeSettings.tsx | 2 +- apps/roam/src/utils/configPageTabs.ts | 2 +- .../utils/getSuggestiveModeConfigSettings.ts | 53 ++++++++++--------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx index 23e948bcb..ec1eabc0c 100644 --- a/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx +++ b/apps/roam/src/components/settings/DiscourseNodeSuggestiveRules.tsx @@ -89,7 +89,7 @@ const DiscourseNodeSuggestiveRules = ({ getSubTree({ parentUid: nodeUid, key: "Template", - }).uid, + }).uid || "", [nodeUid], ); diff --git a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx index bb7097035..7d5baa69a 100644 --- a/apps/roam/src/components/settings/SuggestiveModeSettings.tsx +++ b/apps/roam/src/components/settings/SuggestiveModeSettings.tsx @@ -19,7 +19,7 @@ const SuggestiveModeSettings = () => { if (!settings.suggestiveMode.parentUid) { await createBlock({ parentUid: getPageUidByPageTitle(DISCOURSE_CONFIG_PAGE_TITLE), - node: { text: "Suggestive mode" }, + node: { text: "Suggestive Mode" }, }); refreshConfigTree(); setSettings(getFormattedConfigTree()); diff --git a/apps/roam/src/utils/configPageTabs.ts b/apps/roam/src/utils/configPageTabs.ts index cb7d8aff1..1d43df445 100644 --- a/apps/roam/src/utils/configPageTabs.ts +++ b/apps/roam/src/utils/configPageTabs.ts @@ -118,7 +118,7 @@ export const configPageTabs = (args: OnloadArgs): ConfigTab[] => [ ], }, { - id: "Suggestive mode", + id: "Suggestive Mode", fields: [ { title: "Include Current Page Relations", diff --git a/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts b/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts index a405c9a52..982038443 100644 --- a/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts +++ b/apps/roam/src/utils/getSuggestiveModeConfigSettings.ts @@ -13,31 +13,32 @@ export type SuggestiveModeConfigWithUids = { }; }; -export const getSuggestiveModeConfigAndUids = - (tree: RoamBasicNode[]): SuggestiveModeConfigWithUids => { - const suggestiveModeNode = getSubTree({ - tree, - key: "Suggestive mode", - }); - const pageGroupsNode = getSubTree({ - parentUid: suggestiveModeNode.uid, - key: "Page Groups", - }); +export const getSuggestiveModeConfigAndUids = ( + tree: RoamBasicNode[], +): SuggestiveModeConfigWithUids => { + const suggestiveModeNode = getSubTree({ + tree, + key: "Suggestive Mode", + }); + const pageGroupsNode = getSubTree({ + parentUid: suggestiveModeNode.uid, + key: "Page Groups", + }); - return { - parentUid: suggestiveModeNode.uid, - grabFromReferencedPages: getUidAndBooleanSetting({ - tree: suggestiveModeNode.children, - text: "Include Current Page Relations", - }), - grabParentAndChildren: getUidAndBooleanSetting({ - tree: suggestiveModeNode.children, - text: "Include Parent And Child Blocks", - }), - pageGroups: { - uid: pageGroupsNode?.uid || "", - name: "Page Groups", - pages: [], - }, - }; + return { + parentUid: suggestiveModeNode.uid, + grabFromReferencedPages: getUidAndBooleanSetting({ + tree: suggestiveModeNode.children, + text: "Include Current Page Relations", + }), + grabParentAndChildren: getUidAndBooleanSetting({ + tree: suggestiveModeNode.children, + text: "Include Parent And Child Blocks", + }), + pageGroups: { + uid: pageGroupsNode?.uid || "", + name: "Page Groups", + pages: [], + }, }; +}; From b50cd2e693898ce0aa1a3e6f9867198573b4fcde Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 14 Aug 2025 12:21:02 +0530 Subject: [PATCH 08/44] first working but not good solution --- .../components/DiscourseContextOverlay.tsx | 67 +- .../components/DiscourseSuggestionsPanel.tsx | 429 ++++++++++++ apps/roam/src/components/SuggestionsBody.tsx | 618 ++++++++++++++++++ apps/roam/src/styles/discourseGraphStyles.css | 5 + apps/roam/src/utils/useDiscourseData.ts | 182 ++++++ 5 files changed, 1297 insertions(+), 4 deletions(-) create mode 100644 apps/roam/src/components/DiscourseSuggestionsPanel.tsx create mode 100644 apps/roam/src/components/SuggestionsBody.tsx create mode 100644 apps/roam/src/utils/useDiscourseData.ts diff --git a/apps/roam/src/components/DiscourseContextOverlay.tsx b/apps/roam/src/components/DiscourseContextOverlay.tsx index 25220de50..8809fe33d 100644 --- a/apps/roam/src/components/DiscourseContextOverlay.tsx +++ b/apps/roam/src/components/DiscourseContextOverlay.tsx @@ -15,6 +15,8 @@ import getDiscourseNodes from "~/utils/getDiscourseNodes"; import getDiscourseRelations from "~/utils/getDiscourseRelations"; import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; import { OnloadArgs } from "roamjs-components/types/native"; +import { DiscourseSuggestionsPanel } from "./DiscourseSuggestionsPanel"; +import { getBlockUidFromTarget } from "roamjs-components/dom"; type DiscourseData = { results: Awaited>; @@ -57,12 +59,23 @@ const getOverlayInfo = async (tag: string): Promise => { } }; -const DiscourseContextOverlay = ({ tag, id }: { tag: string; id: string }) => { +const DiscourseContextOverlay = ({ + tag, + id, + parentEl, + onloadArgs, +}: { + tag: string; + id: string; + parentEl: HTMLElement; + onloadArgs: OnloadArgs; +}) => { const tagUid = useMemo(() => getPageUidByPageTitle(tag), [tag]); const [loading, setLoading] = useState(true); const [results, setResults] = useState([]); const [refs, setRefs] = useState(0); const [score, setScore] = useState(0); + const blockUid = useMemo(() => getBlockUidFromTarget(parentEl), [parentEl]); const getInfo = useCallback( () => getOverlayInfo(tag) @@ -94,6 +107,14 @@ const DiscourseContextOverlay = ({ tag, id }: { tag: string; id: string }) => { useEffect(() => { getInfo(); }, [refresh, getInfo]); + + const toggleHighlight = (uid: string, on: boolean) => { + console.log("toggleHighlight", uid, on); + document + .querySelectorAll(`[data-dg-block-uid="${uid}"]`) + .forEach((el) => el.classList.toggle("dg-highlight", on)); + }; + return ( { className={`roamjs-discourse-context-overlay ${ loading ? "animate-pulse" : "" }`} + {...{ "data-dg-block-uid": blockUid }} style={{ minHeight: "initial", paddingTop: ".25rem", @@ -120,12 +142,36 @@ const DiscourseContextOverlay = ({ tag, id }: { tag: string; id: string }) => { }} minimal disabled={loading} + onMouseEnter={() => toggleHighlight(blockUid, true)} + onMouseLeave={() => toggleHighlight(blockUid, false)} >
{loading ? "-" : score} {loading ? "-" : refs} + +
} @@ -134,7 +180,15 @@ const DiscourseContextOverlay = ({ tag, id }: { tag: string; id: string }) => { ); }; -const Wrapper = ({ parent, tag }: { parent: HTMLElement; tag: string }) => { +const Wrapper = ({ + parent, + tag, + onloadArgs, +}: { + parent: HTMLElement; + tag: string; + onloadArgs: OnloadArgs; +}) => { const id = useMemo(() => nanoid(), []); const { inViewport } = useInViewport( { current: parent }, @@ -143,7 +197,12 @@ const Wrapper = ({ parent, tag }: { parent: HTMLElement; tag: string }) => { {}, ); return inViewport ? ( - + ) : (
+ + {selectedPages.length > 0 && ( +
+ {selectedPages.map((p) => ( + { + setSelectedPages((prev) => prev.filter((x) => x !== p)); + if (selectedPages.length === 1) { + setHydeFilteredNodes([]); + setIsSearchingHyde(false); + } + }} + round + minimal + > + {p} + + ))} +
+ )} +
+ + {(hydeFilteredNodes.length > 0 || + isSearchingHyde || + (searchNonce > 0 && + (useAllPagesForSuggestions || selectedPages.length > 0))) && ( +
+
+

+ {useAllPagesForSuggestions + ? "From All Pages" + : selectedPages.length > 0 + ? `From ${selectedPages.length === 1 ? `"${selectedPages[0]}"` : `${selectedPages.length} selected pages`}` + : "Select pages to see suggestions"} +

+ + Total nodes: {actuallyDisplayedNodes.length} + + {availableFilterTypes.length > 1 && ( + + {availableFilterTypes.map((t) => ( +
+ } + > +
+
+
+ {isSearchingHyde && ( + + )} +
    + {!isSearchingHyde && + actuallyDisplayedNodes.length > 0 && + actuallyDisplayedNodes.map((node) => ( +
  • + toggleOverlayHighlight(node.uid, true) + } + onMouseLeave={() => + toggleOverlayHighlight(node.uid, false) + } + > + { + if (e.shiftKey) { + openBlockInSidebar(node.uid); + } else { + window.roamAlphaAPI.ui.mainWindow.openPage({ + page: { uid: node.uid }, + }); + } + }} + > + {node.text} + +
  • + ))} + {!isSearchingHyde && actuallyDisplayedNodes.length === 0 && ( +
  • + {hydeFilteredNodes.length > 0 && + activeNodeTypeFilters.length > 0 + ? "No suggestions match the current filters." + : "No relevant relations found."} +
  • + )} +
+
+
+ + )} + + ); +}; + +export default SuggestionsBody; diff --git a/apps/roam/src/styles/discourseGraphStyles.css b/apps/roam/src/styles/discourseGraphStyles.css index 72859c5d9..b65233b65 100644 --- a/apps/roam/src/styles/discourseGraphStyles.css +++ b/apps/roam/src/styles/discourseGraphStyles.css @@ -177,3 +177,8 @@ div.roamjs-discourse-drawer div.bp3-drawer { .bp3-popover-wrapper + .referenced-node-autocomplete { margin-top: 1rem; } + +.dg-highlight { + box-shadow: 0 0 0 2px #137cbd !important; + border-radius: 4px; +} diff --git a/apps/roam/src/utils/useDiscourseData.ts b/apps/roam/src/utils/useDiscourseData.ts new file mode 100644 index 000000000..94c1dd531 --- /dev/null +++ b/apps/roam/src/utils/useDiscourseData.ts @@ -0,0 +1,182 @@ +import { DiscourseNode } from "./getDiscourseNodes"; +import { DiscourseRelation } from "./getDiscourseRelations"; +import { RelationDetails } from "./hyde"; +import getDiscourseContextResults from "./getDiscourseContextResults"; +import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import findDiscourseNode from "./findDiscourseNode"; +import getDiscourseRelations from "./getDiscourseRelations"; +import getDiscourseNodes from "./getDiscourseNodes"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; +import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; +import deriveDiscourseNodeAttribute from "./deriveDiscourseNodeAttribute"; +import normalizePageTitle from "roamjs-components/queries/normalizePageTitle"; +import { Result } from "roamjs-components/types/query-builder"; + +export type DiscourseData = { + results: Awaited>; + refs: number; +}; + +const cache: { + [tag: string]: DiscourseData; +} = {}; + +export const getOverlayInfo = async ( + tag: string, + relations: ReturnType, +): Promise => { + try { + if (cache[tag]) return cache[tag]; + + const nodes = getDiscourseNodes(relations); + + const [results, refs] = await Promise.all([ + getDiscourseContextResults({ + uid: getPageUidByPageTitle(tag), + nodes, + relations, + }), + await window.roamAlphaAPI.data.async.q( + `[:find ?a :where [?b :node/title "${normalizePageTitle( + tag, + )}"] [?a :block/refs ?b]]`, + ), + ]); + + return (cache[tag] = { + results, + refs: refs.length, + }); + } catch (error) { + console.error(`Error getting overlay info for ${tag}:`, error); + return { + results: [], + refs: 0, + }; + } +}; + +export const useDiscourseData = (tag: string) => { + const [loading, setLoading] = useState(true); + const [results, setResults] = useState([]); + const [refs, setRefs] = useState(0); + const [score, setScore] = useState(0); + + const tagUid = useMemo(() => getPageUidByPageTitle(tag), [tag]); + const discourseNode = useMemo(() => findDiscourseNode(tagUid), [tagUid]); + const relations = useMemo(() => getDiscourseRelations(), []); + const allNodes = useMemo(() => getDiscourseNodes(), []); + + const getInfo = useCallback( + () => + getOverlayInfo(tag, relations) + .then(({ refs, results }) => { + if (!discourseNode) return; + const attribute = getSettingValueFromTree({ + tree: getBasicTreeByParentUid(discourseNode.type), + key: "Overlay", + defaultValue: "Overlay", + }); + return deriveDiscourseNodeAttribute({ + uid: tagUid, + attribute, + }).then((score) => { + setResults(results); + setRefs(refs); + setScore(score); + }); + }) + .finally(() => setLoading(false)), + [tag, tagUid, relations, discourseNode], + ); + + const refresh = useCallback(() => { + setLoading(true); + getInfo(); + }, [getInfo]); + + useEffect(() => { + getInfo(); + }, [getInfo]); + + const validRelations = useMemo(() => { + if (!discourseNode) return []; + const selfType = discourseNode.type; + + return relations.filter( + (relation) => + relation.source === selfType || relation.destination === selfType, + ); + }, [relations, discourseNode]); + + const uniqueRelationTypeTriplets = useMemo(() => { + if (!discourseNode) return []; + const relatedNodeType = discourseNode.type; + + return validRelations.flatMap((relation) => { + const isSelfSource = relation.source === relatedNodeType; + const isSelfDestination = relation.destination === relatedNodeType; + + let targetNodeType: string; + let currentRelationLabel: string; + + if (isSelfSource) { + targetNodeType = relation.destination; + currentRelationLabel = relation.label; + } else if (isSelfDestination) { + targetNodeType = relation.source; + currentRelationLabel = relation.complement; + } else { + return []; + } + + const identifiedTargetNode = allNodes.find( + (node) => node.type === targetNodeType, + ); + if (!identifiedTargetNode) return []; + + return [ + { + relationLabel: currentRelationLabel, + relatedNodeText: identifiedTargetNode.text, + relatedNodeFormat: identifiedTargetNode.format, + }, + ]; + }); + }, [validRelations, discourseNode, allNodes]); + + const validTypes = useMemo(() => { + if (!discourseNode) return []; + const selfType = discourseNode.type; + + const hasSelfRelation = validRelations.some( + (relation) => + relation.source === selfType && relation.destination === selfType, + ); + const types = Array.from( + new Set( + validRelations.flatMap((relation) => [ + relation.source, + relation.destination, + ]), + ), + ); + return hasSelfRelation ? types : types.filter((type) => type !== selfType); + }, [discourseNode, validRelations]); + + return { + loading, + results, + refs, + score, + refresh, + tagUid, + discourseNode, + relations, + allNodes, + validRelations, + uniqueRelationTypeTriplets, + validTypes, + }; +}; From b8fce338afc98f208f3d4cb9c8508e92d17c8ba7 Mon Sep 17 00:00:00 2001 From: sid597 Date: Thu, 14 Aug 2025 17:05:25 +0530 Subject: [PATCH 09/44] working better code need to self review, use --full instead of --large --- .../components/DiscourseContextOverlay.tsx | 130 ++--- .../components/DiscourseSuggestionsPanel.tsx | 345 +---------- apps/roam/src/components/PanelManager.tsx | 538 ++++++++++++++++++ 3 files changed, 598 insertions(+), 415 deletions(-) create mode 100644 apps/roam/src/components/PanelManager.tsx diff --git a/apps/roam/src/components/DiscourseContextOverlay.tsx b/apps/roam/src/components/DiscourseContextOverlay.tsx index 8809fe33d..89ff7d1dd 100644 --- a/apps/roam/src/components/DiscourseContextOverlay.tsx +++ b/apps/roam/src/components/DiscourseContextOverlay.tsx @@ -1,3 +1,4 @@ +// DiscourseContextOverlay.tsx import { Button, Icon, Popover, Position, Tooltip } from "@blueprintjs/core"; import React, { useCallback, useEffect, useMemo, useState } from "react"; import ReactDOM from "react-dom"; @@ -15,8 +16,9 @@ import getDiscourseNodes from "~/utils/getDiscourseNodes"; import getDiscourseRelations from "~/utils/getDiscourseRelations"; import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; import { OnloadArgs } from "roamjs-components/types/native"; -import { DiscourseSuggestionsPanel } from "./DiscourseSuggestionsPanel"; import { getBlockUidFromTarget } from "roamjs-components/dom"; +import { useDiscourseData } from "~/utils/useDiscourseData"; +import { PanelManager } from "./PanelManager"; type DiscourseData = { results: Awaited>; @@ -27,38 +29,6 @@ const cache: { [tag: string]: DiscourseData; } = {}; -const getOverlayInfo = async (tag: string): Promise => { - try { - if (cache[tag]) return cache[tag]; - - const relations = getDiscourseRelations(); - const nodes = getDiscourseNodes(relations); - - const [results, refs] = await Promise.all([ - getDiscourseContextResults({ - uid: getPageUidByPageTitle(tag), - nodes, - relations, - }), - // @ts-ignore - backend to be added to roamjs-components - window.roamAlphaAPI.data.backend.q( - `[:find ?a :where [?b :node/title "${normalizePageTitle(tag)}"] [?a :block/refs ?b]]`, - ), - ]); - - return (cache[tag] = { - results, - refs: refs.length, - }); - } catch (error) { - console.error(`Error getting overlay info for ${tag}:`, error); - return { - results: [], - refs: 0, - }; - } -}; - const DiscourseContextOverlay = ({ tag, id, @@ -70,50 +40,40 @@ const DiscourseContextOverlay = ({ parentEl: HTMLElement; onloadArgs: OnloadArgs; }) => { - const tagUid = useMemo(() => getPageUidByPageTitle(tag), [tag]); - const [loading, setLoading] = useState(true); - const [results, setResults] = useState([]); - const [refs, setRefs] = useState(0); - const [score, setScore] = useState(0); const blockUid = useMemo(() => getBlockUidFromTarget(parentEl), [parentEl]); - const getInfo = useCallback( - () => - getOverlayInfo(tag) - .then(({ refs, results }) => { - const discourseNode = findDiscourseNode(tagUid); - if (discourseNode) { - const attribute = getSettingValueFromTree({ - tree: getBasicTreeByParentUid(discourseNode.type), - key: "Overlay", - defaultValue: "Overlay", - }); - return deriveDiscourseNodeAttribute({ - uid: tagUid, - attribute, - }).then((score) => { - setResults(results); - setRefs(refs); - setScore(score); - }); - } - }) - .finally(() => setLoading(false)), - [tag, setResults, setLoading, setRefs, setScore], + const { loading, score, refs, results, tagUid } = useDiscourseData(tag); + const [isPanelOpen, setIsPanelOpen] = useState(() => + PanelManager.isOpen(tag), ); - const refresh = useCallback(() => { - setLoading(true); - getInfo(); - }, [getInfo, setLoading]); + + // Subscribe to panel state changes useEffect(() => { - getInfo(); - }, [refresh, getInfo]); + const unsubscribe = PanelManager.subscribe((openTags) => { + setIsPanelOpen(openTags.includes(tag)); + }); + return () => { + unsubscribe(); + }; + }, [tag]); - const toggleHighlight = (uid: string, on: boolean) => { - console.log("toggleHighlight", uid, on); - document - .querySelectorAll(`[data-dg-block-uid="${uid}"]`) - .forEach((el) => el.classList.toggle("dg-highlight", on)); - }; + const toggleHighlight = useCallback( + (on: boolean) => { + console.log("toggleHighlight", blockUid, on); + document + .querySelectorAll(`[data-dg-block-uid="${blockUid}"]`) + .forEach((el) => el.classList.toggle("dg-highlight", on)); + }, + [blockUid], + ); + + const handleTogglePanel = useCallback( + (e: React.MouseEvent) => { + e.stopPropagation(); + e.preventDefault(); + PanelManager.toggle(tag, blockUid, parentEl, onloadArgs); + }, + [tag, blockUid, parentEl, onloadArgs], + ); return ( toggleHighlight(blockUid, true)} - onMouseLeave={() => toggleHighlight(blockUid, false)} + onMouseEnter={() => toggleHighlight(true)} + onMouseLeave={() => toggleHighlight(false)} >
@@ -151,25 +111,21 @@ const DiscourseContextOverlay = ({ {loading ? "-" : refs}
diff --git a/apps/roam/src/components/DiscourseSuggestionsPanel.tsx b/apps/roam/src/components/DiscourseSuggestionsPanel.tsx index 5c92fdf28..8c5513225 100644 --- a/apps/roam/src/components/DiscourseSuggestionsPanel.tsx +++ b/apps/roam/src/components/DiscourseSuggestionsPanel.tsx @@ -1,6 +1,3 @@ - - - import { Alignment, Card, @@ -9,69 +6,32 @@ import { Navbar, Collapse, } from "@blueprintjs/core"; -import React, { useMemo, useState } from "react"; -import ReactDOM from "react-dom"; -import getDiscourseContextResults from "~/utils/getDiscourseContextResults"; -import { getBlockUidFromTarget } from "roamjs-components/dom"; +import React, { useMemo, useState, useCallback } from "react"; import SuggestionsBody from "./SuggestionsBody"; import { useDiscourseData } from "~/utils/useDiscourseData"; -import { OnloadArgs } from "roamjs-components/types/native"; -import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; - -const PANEL_ROOT_ID = "discourse-graph-suggestions-root"; -const PANELS_CONTAINER_ID = "discourse-graph-panels-container"; -const ARTICLE_WRAPPER_SELECTOR = ".rm-article-wrapper"; -let articleWrapperObserver: MutationObserver | null = null; - -const initializeObserver = (mainContent: HTMLElement) => { - if (articleWrapperObserver) { - return; - } - articleWrapperObserver = new MutationObserver(() => { - const root = document.getElementById(PANEL_ROOT_ID); - if (root && root.style.display !== "none") { - if (!mainContent.classList.contains("rm-spacing--large")) { - mainContent.classList.add("rm-spacing--large"); - mainContent.classList.remove("rm-spacing--small"); - } - } - }); - articleWrapperObserver.observe(mainContent, { - attributes: true, - attributeFilter: ["class"], - }); -}; - -type DiscourseData = { - results: Awaited>; - refs: number; -}; -const cache: { - [tag: string]: DiscourseData; -} = {}; - -const toggleHighlight = (uid: string, on: boolean) => { - console.log("toggleHighlight", uid, on); - document - .querySelectorAll(`[data-dg-block-uid="${uid}"]`) - .forEach((el) => el.classList.toggle("dg-highlight", on)); -}; export const DiscourseSuggestionsPanel = ({ - onClose, tag, - id, - parentEl, + blockUid, + onClose, }: { - onClose: () => void; tag: string; - id: string; - parentEl: HTMLElement; + blockUid: string; + onClose: () => void; }) => { - const blockUid = useMemo(() => getBlockUidFromTarget(parentEl), [parentEl]); const [isOpen, setIsOpen] = useState(true); const { results } = useDiscourseData(tag); + const toggleHighlight = useCallback( + (on: boolean) => { + console.log("toggleHighlight", blockUid, on); + document + .querySelectorAll(`[data-dg-block-uid="${blockUid}"]`) + .forEach((el) => el.classList.toggle("dg-highlight", on)); + }, + [blockUid], + ); + return ( toggleHighlight(blockUid, true)} - onMouseLeave={() => toggleHighlight(blockUid, false)} + onMouseEnter={() => toggleHighlight(true)} + onMouseLeave={() => toggleHighlight(false)} className="roamjs-discourse-suggestions-panel" > ); }; - -// Static method to toggle the suggestions panel -DiscourseSuggestionsPanel.toggle = ( - tag: string, - id: string, - parentEl: HTMLElement, - onloadArgs: OnloadArgs, -) => { - // Ensure there is a dedicated root element for all suggestion panels. - let suggestionsRoot = document.getElementById( - PANEL_ROOT_ID, - ) as HTMLElement | null; - - // Always reference Roam's main container – we need it for (un)split logic - const roamBodyMain = document.querySelector( - ".roam-body-main", - ) as HTMLElement | null; - - // If the root does not exist yet, create it and apply the 40/60 split. - if (!suggestionsRoot && roamBodyMain) { - const mainContent = roamBodyMain.querySelector( - ARTICLE_WRAPPER_SELECTOR, - ); - if (!mainContent) return; // safety-guard – shouldn't happen in a normal Roam page - - suggestionsRoot = document.createElement("div"); - suggestionsRoot.id = PANEL_ROOT_ID; - suggestionsRoot.style.display = "flex"; - suggestionsRoot.style.flexDirection = "column"; - suggestionsRoot.style.flex = "0 0 40%"; - - // Insert the root before Roam's main content area and apply split styling - roamBodyMain.insertBefore(suggestionsRoot, mainContent); - roamBodyMain.style.display = "flex"; - mainContent.style.flex = "1 1 60%"; - roamBodyMain.dataset.isSplit = "true"; - - if (mainContent) { - mainContent.classList.remove("rm-spacing--small"); - mainContent.classList.add("rm-spacing--large"); - initializeObserver(mainContent); - } - } - - // If we still don't have either container, bail - if (!suggestionsRoot) return; - - // If the root exists but is currently hidden, show it again and re-apply - // the 40/60 split layout that we use for split view. - if ( - suggestionsRoot.style.display === "none" && - roamBodyMain && - !roamBodyMain.dataset.isSplit - ) { - const mainContent = roamBodyMain.querySelector( - ARTICLE_WRAPPER_SELECTOR, - ); - suggestionsRoot.style.display = "flex"; - // Ensure the root is sized correctly. - suggestionsRoot.style.flex = "0 0 40%"; - - // Apply flex split styling to the parent container and main content. - roamBodyMain.style.display = "flex"; - if (mainContent && mainContent !== suggestionsRoot) { - mainContent.style.flex = "1 1 60%"; - } - roamBodyMain.dataset.isSplit = "true"; - if (mainContent) { - mainContent.classList.remove("rm-spacing--small"); - mainContent.classList.add("rm-spacing--large"); - initializeObserver(mainContent); - } - } - - // From now on, always append the panels container to `suggestionsRoot`. - const containerParent = suggestionsRoot; - - const panelId = `discourse-panel-${tag.replace(/[^a-zA-Z0-9]/g, "-")}`; - const existingPanel = document.getElementById(panelId); - - // If this specific panel already exists, close only this panel - if (existingPanel) { - ReactDOM.unmountComponentAtNode(existingPanel); - existingPanel.remove(); - - // Check if there are any remaining panels - const panelsContainer = document.getElementById( - PANELS_CONTAINER_ID, - ) as HTMLElement | null; - const remainingPanels = panelsContainer?.children.length || 0; - - if (remainingPanels <= 1 && panelsContainer) { - panelsContainer.remove(); - // Remove the suggestions root and restore layout - if (suggestionsRoot?.parentElement) { - suggestionsRoot.remove(); - } - if (roamBodyMain && roamBodyMain.dataset.isSplit === "true") { - roamBodyMain.removeAttribute("data-is-split"); - roamBodyMain.style.display = ""; - const mainContent = roamBodyMain.querySelector( - ARTICLE_WRAPPER_SELECTOR, - ); - if (mainContent) { - mainContent.style.flex = ""; - mainContent.classList.remove("rm-spacing--large"); - mainContent.classList.add("rm-spacing--small"); - } - } - if (articleWrapperObserver) { - articleWrapperObserver.disconnect(); - articleWrapperObserver = null; - } - } - return; - } - - // Ensure there is only one panels container in the entire document - let panelsContainer = document.getElementById( - PANELS_CONTAINER_ID, - ) as HTMLElement | null; - - // If a container exists but is NOT inside the intended parent, move it. - if (panelsContainer && panelsContainer.parentElement !== containerParent) { - panelsContainer.parentElement?.removeChild(panelsContainer); - containerParent.appendChild(panelsContainer); - } - - // Create the panels container if it does not exist yet - if (!panelsContainer) { - panelsContainer = document.createElement("div"); - panelsContainer.id = PANELS_CONTAINER_ID; - panelsContainer.style.display = "flex"; - panelsContainer.style.flexDirection = "column"; - panelsContainer.style.flex = "1 1 auto"; - panelsContainer.style.gap = "8px"; - panelsContainer.style.padding = "8px"; - panelsContainer.style.backgroundColor = "#f5f5f5"; - panelsContainer.style.overflowY = "auto"; - - containerParent.appendChild(panelsContainer); - - // Common header shown once per container - const headerCardId = "discourse-suggestions-header"; - const headerCard = document.createElement("div"); - headerCard.id = headerCardId; - headerCard.style.flex = "0 0 auto"; - headerCard.style.padding = "6px 8px"; - headerCard.style.backgroundColor = "#fff"; - headerCard.style.borderRadius = "4px 4px 0 0"; - headerCard.style.marginBottom = "0"; - headerCard.style.fontWeight = "600"; - headerCard.style.fontSize = "13px"; - headerCard.style.boxShadow = "0 1px 3px rgba(0,0,0,0.08)"; - headerCard.style.display = "flex"; - headerCard.style.justifyContent = "space-between"; - headerCard.style.alignItems = "center"; - - panelsContainer.appendChild(headerCard); - - const headerTitle = document.createElement("span"); - headerTitle.textContent = "Suggested Discourse nodes"; - - const closeSidebarBtn = document.createElement("button"); - closeSidebarBtn.textContent = "✕"; - closeSidebarBtn.style.cursor = "pointer"; - closeSidebarBtn.style.border = "none"; - closeSidebarBtn.style.background = "transparent"; - closeSidebarBtn.title = "Close sidebar"; - - closeSidebarBtn.onclick = () => { - // Simulate clicking Split View button if present to close - const splitBtn = document.querySelector( - '[title="Split View"]', - ) as HTMLElement | null; - if (splitBtn) { - splitBtn.click(); - } else { - // Fallback: manually hide suggestions root and restore layout - const panelRoot = document.getElementById( - PANEL_ROOT_ID, - ) as HTMLElement | null; - if (panelRoot) { - panelRoot.style.display = "none"; - } - const roamBodyMain = document.querySelector( - ".roam-body-main", - ) as HTMLElement | null; - if (roamBodyMain) { - roamBodyMain.removeAttribute("data-is-split"); - roamBodyMain.style.display = ""; - const mainContent = - panelRoot?.nextElementSibling as HTMLElement | null; - if (mainContent) { - mainContent.style.flex = ""; - mainContent.classList.remove("rm-spacing--large"); - mainContent.classList.add("rm-spacing--small"); - } - } - } - if (articleWrapperObserver) { - articleWrapperObserver.disconnect(); - articleWrapperObserver = null; - } - }; - - headerCard.appendChild(headerTitle); - headerCard.appendChild(closeSidebarBtn); - } - - // Create the new panel - const newPanel = document.createElement("div"); - newPanel.id = panelId; - newPanel.style.flex = "0 0 auto"; - newPanel.style.marginBottom = "8px"; - newPanel.style.marginTop = "0"; - newPanel.style.backgroundColor = "#fff"; - newPanel.style.borderRadius = "0 0 4px 4px"; - newPanel.style.boxShadow = "0 1px 3px rgba(0,0,0,0.1)"; - - const header = panelsContainer.querySelector("#discourse-suggestions-header"); - if (header) { - header.insertAdjacentElement("afterend", newPanel); - } else { - panelsContainer.appendChild(newPanel); - } - - const handleClosePanel = () => { - ReactDOM.unmountComponentAtNode(newPanel); - newPanel.remove(); - - // Check if there are any remaining panels - const remainingPanels = panelsContainer?.children.length || 0; - - if (remainingPanels === 1 && newPanel.parentElement === panelsContainer) { - panelsContainer.remove(); - // Remove the suggestions root and restore layout - if (suggestionsRoot?.parentElement) { - suggestionsRoot.remove(); - } - if (roamBodyMain && roamBodyMain.dataset.isSplit === "true") { - roamBodyMain.removeAttribute("data-is-split"); - roamBodyMain.style.display = ""; - const mainContent = roamBodyMain.querySelector( - ARTICLE_WRAPPER_SELECTOR, - ); - if (mainContent) { - mainContent.style.flex = ""; - mainContent.classList.remove("rm-spacing--large"); - mainContent.classList.add("rm-spacing--small"); - } - } - if (articleWrapperObserver) { - articleWrapperObserver.disconnect(); - articleWrapperObserver = null; - } - } - }; - - ReactDOM.render( - - - , - newPanel, - ); -}; diff --git a/apps/roam/src/components/PanelManager.tsx b/apps/roam/src/components/PanelManager.tsx new file mode 100644 index 000000000..4c9feb754 --- /dev/null +++ b/apps/roam/src/components/PanelManager.tsx @@ -0,0 +1,538 @@ +import React from "react"; +import ReactDOM from "react-dom"; +import { OnloadArgs } from "roamjs-components/types/native"; +import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; +import { DiscourseSuggestionsPanel } from "./DiscourseSuggestionsPanel"; + +// Constants +const PANEL_ROOT_ID = "discourse-graph-suggestions-root"; +const PANELS_CONTAINER_ID = "discourse-graph-panels-container"; +const ARTICLE_WRAPPER_SELECTOR = ".rm-article-wrapper"; +const MINIMIZED_BAR_ID = "discourse-suggestions-minimized"; + +// Track open panels globally +type PanelState = { + blockUid: string; + element: HTMLElement; + onloadArgs: OnloadArgs; +}; +const openPanels = new Map(); +let isContainerMinimized = false; +let articleWrapperObserver: MutationObserver | null = null; +let navigationObserver: MutationObserver | null = null; + +// Initialize observer for article wrapper changes +const initializeObserver = (mainContent: HTMLElement) => { + if (articleWrapperObserver) { + return; + } + articleWrapperObserver = new MutationObserver(() => { + const root = document.getElementById(PANEL_ROOT_ID); + if (root && root.style.display !== "none" && !isContainerMinimized) { + if (!mainContent.classList.contains("rm-spacing--full")) { + mainContent.classList.add("rm-spacing--full"); + mainContent.classList.remove("rm-spacing--small"); + } + } + }); + articleWrapperObserver.observe(mainContent, { + attributes: true, + attributeFilter: ["class"], + }); +}; + +// Initialize observer for navigation changes +const initializeNavigationObserver = () => { + if (navigationObserver) { + return; + } + + const roamApp = document.querySelector(".roam-app"); + if (!roamApp) return; + + navigationObserver = new MutationObserver((mutations) => { + // Check if we still have our panel root + const panelRoot = document.getElementById(PANEL_ROOT_ID); + const roamBodyMain = getRoamBodyMain(); + + // If we have open panels but the infrastructure is gone, recreate it + if (openPanels.size > 0 && (!panelRoot || !panelRoot.parentElement)) { + restorePanelInfrastructure(); + } + + // Ensure split view is maintained if panels are open + if (openPanels.size > 0 && roamBodyMain && panelRoot) { + const articleWrapper = getArticleWrapper(roamBodyMain); + if (articleWrapper && roamBodyMain.dataset.isSplit !== "true") { + setupSplitView(roamBodyMain, articleWrapper, panelRoot); + } + } + }); + + navigationObserver.observe(roamApp, { + childList: true, + subtree: true, + }); +}; + +// Utility functions +const getRoamBodyMain = () => + document.querySelector(".roam-body-main") as HTMLElement | null; + +const getArticleWrapper = (container: HTMLElement) => + container.querySelector(ARTICLE_WRAPPER_SELECTOR); + +const setupSplitView = ( + roamBodyMain: HTMLElement, + articleWrapper: HTMLElement, + panelRoot: HTMLElement, +) => { + roamBodyMain.style.display = "flex"; + roamBodyMain.dataset.isSplit = "true"; + + panelRoot.style.display = "flex"; + panelRoot.style.flexDirection = "column"; + panelRoot.style.flex = "0 0 40%"; + + articleWrapper.style.flex = "1 1 60%"; + articleWrapper.classList.remove("rm-spacing--small"); + articleWrapper.classList.add("rm-spacing--full"); + + initializeObserver(articleWrapper); +}; + +const teardownSplitView = () => { + const roamBodyMain = getRoamBodyMain(); + const articleWrapper = roamBodyMain ? getArticleWrapper(roamBodyMain) : null; + + if (roamBodyMain && articleWrapper) { + roamBodyMain.removeAttribute("data-is-split"); + roamBodyMain.style.display = ""; + articleWrapper.style.flex = ""; + articleWrapper.classList.remove("rm-spacing--full"); + articleWrapper.classList.add("rm-spacing--small"); + } + + if (articleWrapperObserver) { + articleWrapperObserver.disconnect(); + articleWrapperObserver = null; + } +}; + +// Minimized bar helpers +const createMinimizedBar = (panelRoot: HTMLElement) => { + let minimizedBar = document.getElementById( + MINIMIZED_BAR_ID, + ) as HTMLElement | null; + if (minimizedBar) return minimizedBar; + + minimizedBar = document.createElement("div"); + minimizedBar.id = MINIMIZED_BAR_ID; + minimizedBar.style.cssText = ` + display: flex; + align-items: center; + gap: 4px; + padding: 6px 8px; + background: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); + margin: 8px; + width: fit-content; + `; + + const restoreButton = document.createElement("button"); + restoreButton.style.cssText = ` + cursor: pointer; + border: none; + background: transparent; + padding: 2px 6px; + `; + restoreButton.title = "Restore sidebar"; + restoreButton.onclick = () => PanelManager.toggleContainerMinimize(); + + const icon = document.createElement("span"); + icon.className = "bp3-icon bp3-icon-panel-stats"; + restoreButton.appendChild(icon); + + minimizedBar.appendChild(restoreButton); + panelRoot.appendChild(minimizedBar); + + return minimizedBar; +}; + +const removeMinimizedBar = () => { + const minimizedBar = document.getElementById(MINIMIZED_BAR_ID); + if (minimizedBar) minimizedBar.remove(); +}; + +const createPanelInfrastructure = () => { + const roamBodyMain = getRoamBodyMain(); + if (!roamBodyMain) return null; + + const articleWrapper = getArticleWrapper(roamBodyMain); + if (!articleWrapper) return null; + + // Check if panel root already exists (might have been hidden) + let panelRoot = document.getElementById(PANEL_ROOT_ID) as HTMLElement | null; + + if (panelRoot) { + // If it exists but is hidden, show it + if (panelRoot.style.display === "none") { + panelRoot.style.display = "flex"; + } + // If it's not in the right place, move it + if (panelRoot.parentElement !== roamBodyMain) { + roamBodyMain.insertBefore(panelRoot, articleWrapper); + } + } else { + // Create panel root + panelRoot = document.createElement("div"); + panelRoot.id = PANEL_ROOT_ID; + roamBodyMain.insertBefore(panelRoot, articleWrapper); + } + + // Check if panels container exists + let panelsContainer = document.getElementById( + PANELS_CONTAINER_ID, + ) as HTMLElement | null; + + if (!panelsContainer) { + // Create panels container + panelsContainer = document.createElement("div"); + panelsContainer.id = PANELS_CONTAINER_ID; + panelsContainer.style.cssText = ` + display: flex; + flex-direction: column; + flex: 1 1 auto; + gap: 8px; + padding: 8px; + background-color: #f5f5f5; + overflow-y: auto; + `; + + // Create header + const header = document.createElement("div"); + header.id = "discourse-suggestions-header"; + header.style.cssText = ` + flex: 0 0 auto; + padding: 6px 8px; + background-color: #fff; + border-radius: 4px 4px 0 0; + margin-bottom: 0; + font-weight: 600; + font-size: 13px; + box-shadow: 0 1px 3px rgba(0,0,0,0.08); + display: flex; + justify-content: space-between; + align-items: center; + `; + + const headerTitle = document.createElement("span"); + headerTitle.textContent = "Suggested Discourse nodes"; + + const headerButtons = document.createElement("div"); + headerButtons.style.cssText = ` + display: flex; + gap: 4px; + align-items: center; + `; + + const minimizeButton = document.createElement("button"); + minimizeButton.innerHTML = "⎯"; + minimizeButton.style.cssText = ` + cursor: pointer; + border: none; + background: transparent; + padding: 2px 6px; + font-size: 16px; + `; + minimizeButton.title = "Minimize sidebar"; + minimizeButton.onclick = () => PanelManager.toggleContainerMinimize(); + + const closeButton = document.createElement("button"); + closeButton.textContent = "✕"; + closeButton.style.cssText = ` + cursor: pointer; + border: none; + background: transparent; + padding: 2px 6px; + `; + closeButton.title = "Close all open panels"; + closeButton.onclick = () => PanelManager.closeAll(); + + header.appendChild(headerTitle); + headerButtons.appendChild(minimizeButton); + headerButtons.appendChild(closeButton); + header.appendChild(headerButtons); + panelsContainer.appendChild(header); + + panelRoot.appendChild(panelsContainer); + } + + // Apply split view + setupSplitView(roamBodyMain, articleWrapper, panelRoot); + + // Initialize navigation observer + initializeNavigationObserver(); + + return panelsContainer; +}; + +const restorePanelInfrastructure = () => { + // Don't restore if no panels are open + if (openPanels.size === 0) return; + + const panelsContainer = createPanelInfrastructure(); + if (!panelsContainer) return; + + // Recreate all open panels + const panelsToRestore = Array.from(openPanels.entries()); + openPanels.clear(); + + panelsToRestore.forEach(([tag, state]) => { + // Create new panel element + const panelElement = document.createElement("div"); + panelElement.id = `discourse-panel-${tag.replace(/[^a-zA-Z0-9]/g, "-")}`; + panelElement.style.cssText = ` + flex: 0 0 auto; + margin-bottom: 8px; + background-color: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + `; + + // Insert after header + const header = panelsContainer.querySelector( + "#discourse-suggestions-header", + ); + if (header && header.nextSibling) { + panelsContainer.insertBefore(panelElement, header.nextSibling); + } else { + panelsContainer.appendChild(panelElement); + } + + // Update state with new element + openPanels.set(tag, { ...state, element: panelElement }); + + // Render React component + ReactDOM.render( + + PanelManager.removePanel(tag)} + /> + , + panelElement, + ); + }); + + // Restore minimized state if needed + if (isContainerMinimized) { + PanelManager.toggleContainerMinimize(); + } +}; + +const cleanupPanelInfrastructure = () => { + // Unmount all React components + openPanels.forEach(({ element }) => { + ReactDOM.unmountComponentAtNode(element); + }); + openPanels.clear(); + + // Remove DOM elements (but keep them in DOM if hidden for potential restoration) + const panelRoot = document.getElementById(PANEL_ROOT_ID); + if (panelRoot) { + panelRoot.remove(); + } + + // Restore layout + teardownSplitView(); + + // Disconnect observers + if (navigationObserver) { + navigationObserver.disconnect(); + navigationObserver = null; + } +}; + +// Main Panel Manager +export const PanelManager = { + toggle: ( + tag: string, + blockUid: string, + parentEl: HTMLElement, + onloadArgs: OnloadArgs, + ) => { + // If this panel is already open, close it + if (openPanels.has(tag)) { + PanelManager.removePanel(tag); + return; + } + + // Add the panel + PanelManager.addPanel(tag, blockUid, parentEl, onloadArgs); + }, + + addPanel: ( + tag: string, + blockUid: string, + parentEl: HTMLElement, + onloadArgs: OnloadArgs, + ) => { + // Get or create infrastructure + let panelsContainer = document.getElementById(PANELS_CONTAINER_ID); + if (!panelsContainer) { + panelsContainer = createPanelInfrastructure(); + if (!panelsContainer) return; // Failed to create + } + + // Create panel element + const panelElement = document.createElement("div"); + panelElement.id = `discourse-panel-${tag.replace(/[^a-zA-Z0-9]/g, "-")}`; + panelElement.style.cssText = ` + flex: 0 0 auto; + margin-bottom: 8px; + background-color: #fff; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0,0,0,0.1); + `; + + // Insert after header (new panels on top) + const header = panelsContainer.querySelector( + "#discourse-suggestions-header", + ); + if (header && header.nextSibling) { + panelsContainer.insertBefore(panelElement, header.nextSibling); + } else { + panelsContainer.appendChild(panelElement); + } + + // Track this panel with onloadArgs for restoration + openPanels.set(tag, { blockUid, element: panelElement, onloadArgs }); + + // Render React component + ReactDOM.render( + + PanelManager.removePanel(tag)} + /> + , + panelElement, + ); + + // Notify listeners about panel state change + PanelManager.notifyStateChange(); + }, + + removePanel: (tag: string) => { + const panelInfo = openPanels.get(tag); + if (!panelInfo) return; + + // Unmount React and remove element + ReactDOM.unmountComponentAtNode(panelInfo.element); + panelInfo.element.remove(); + openPanels.delete(tag); + + // If no panels left, cleanup everything + if (openPanels.size === 0) { + cleanupPanelInfrastructure(); + } + + // Notify listeners about panel state change + PanelManager.notifyStateChange(); + }, + + closeAll: () => { + isContainerMinimized = false; + cleanupPanelInfrastructure(); + PanelManager.notifyStateChange(); + }, + + toggleContainerMinimize: () => { + const panelsContainer = document.getElementById(PANELS_CONTAINER_ID); + const panelRoot = document.getElementById(PANEL_ROOT_ID); + + if (!panelsContainer || !panelRoot) return; + + isContainerMinimized = !isContainerMinimized; + + if (isContainerMinimized) { + // Hide the full container and show a compact minimized bar + (panelsContainer as HTMLElement).style.display = "none"; + + // Shrink the panel root width and add minimized bar + panelRoot.style.flex = "0 0 auto"; + panelRoot.style.width = "auto"; + createMinimizedBar(panelRoot as HTMLElement); + + // Adjust main content + const roamBodyMain = getRoamBodyMain(); + const articleWrapper = roamBodyMain + ? getArticleWrapper(roamBodyMain) + : null; + if (articleWrapper) { + articleWrapper.style.flex = "1 1 auto"; + } + } else { + // Restore full container + (panelsContainer as HTMLElement).style.display = ""; + + // Remove minimized bar + removeMinimizedBar(); + + // Restore panel root width + panelRoot.style.flex = "0 0 40%"; + panelRoot.style.width = ""; + + // Update minimize button, if present + const minimizeBtn = panelsContainer.querySelector( + 'button[title="Restore sidebar"], button[title="Minimize sidebar"]', + ); + if (minimizeBtn) { + minimizeBtn.innerHTML = "⎯"; + minimizeBtn.title = "Minimize sidebar"; + } + + // Restore split view + const roamBodyMain = getRoamBodyMain(); + const articleWrapper = roamBodyMain + ? getArticleWrapper(roamBodyMain) + : null; + if (articleWrapper) { + articleWrapper.style.flex = "1 1 60%"; + } + } + + PanelManager.notifyStateChange(); + }, + + isOpen: (tag: string) => { + return openPanels.has(tag); + }, + + isContainerMinimized: () => { + return isContainerMinimized; + }, + + // Simple event system for state changes + listeners: new Set< + (openTags: string[], containerMinimized: boolean) => void + >(), + + subscribe: ( + callback: (openTags: string[], containerMinimized: boolean) => void, + ) => { + PanelManager.listeners.add(callback); + return () => PanelManager.listeners.delete(callback); + }, + + notifyStateChange: () => { + const openTags = Array.from(openPanels.keys()); + PanelManager.listeners.forEach((callback) => + callback(openTags, isContainerMinimized), + ); + }, +}; From 622de132d67f3ba43e7554c11a8320e1cd020de5 Mon Sep 17 00:00:00 2001 From: sid597 Date: Sat, 16 Aug 2025 15:51:36 +0530 Subject: [PATCH 10/44] seems good, will find after review what i missed --- .../components/DiscourseContextOverlay.tsx | 40 +- .../components/DiscourseSuggestionsPanel.tsx | 9 +- apps/roam/src/components/PanelManager.tsx | 602 ++++++------ apps/roam/src/components/SuggestionsBody.tsx | 858 +++++++----------- apps/roam/src/index.ts | 5 +- apps/roam/src/utils/useDiscourseData.ts | 23 +- 6 files changed, 595 insertions(+), 942 deletions(-) diff --git a/apps/roam/src/components/DiscourseContextOverlay.tsx b/apps/roam/src/components/DiscourseContextOverlay.tsx index 89ff7d1dd..191ebdab4 100644 --- a/apps/roam/src/components/DiscourseContextOverlay.tsx +++ b/apps/roam/src/components/DiscourseContextOverlay.tsx @@ -1,33 +1,14 @@ -// DiscourseContextOverlay.tsx import { Button, Icon, Popover, Position, Tooltip } from "@blueprintjs/core"; -import React, { useCallback, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useMemo, useState } from "react"; import ReactDOM from "react-dom"; import { ContextContent } from "./DiscourseContext"; import useInViewport from "react-in-viewport/dist/es/lib/useInViewport"; -import normalizePageTitle from "roamjs-components/queries/normalizePageTitle"; -import deriveDiscourseNodeAttribute from "~/utils/deriveDiscourseNodeAttribute"; -import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; -import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import nanoid from "nanoid"; -import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; -import getDiscourseContextResults from "~/utils/getDiscourseContextResults"; -import findDiscourseNode from "~/utils/findDiscourseNode"; -import getDiscourseNodes from "~/utils/getDiscourseNodes"; -import getDiscourseRelations from "~/utils/getDiscourseRelations"; import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; import { OnloadArgs } from "roamjs-components/types/native"; import { getBlockUidFromTarget } from "roamjs-components/dom"; import { useDiscourseData } from "~/utils/useDiscourseData"; -import { PanelManager } from "./PanelManager"; - -type DiscourseData = { - results: Awaited>; - refs: number; -}; - -const cache: { - [tag: string]: DiscourseData; -} = {}; +import { panelManager } from "./PanelManager"; const DiscourseContextOverlay = ({ tag, @@ -43,19 +24,9 @@ const DiscourseContextOverlay = ({ const blockUid = useMemo(() => getBlockUidFromTarget(parentEl), [parentEl]); const { loading, score, refs, results, tagUid } = useDiscourseData(tag); const [isPanelOpen, setIsPanelOpen] = useState(() => - PanelManager.isOpen(tag), + panelManager.isOpen(tag), ); - // Subscribe to panel state changes - useEffect(() => { - const unsubscribe = PanelManager.subscribe((openTags) => { - setIsPanelOpen(openTags.includes(tag)); - }); - return () => { - unsubscribe(); - }; - }, [tag]); - const toggleHighlight = useCallback( (on: boolean) => { console.log("toggleHighlight", blockUid, on); @@ -70,9 +41,10 @@ const DiscourseContextOverlay = ({ (e: React.MouseEvent) => { e.stopPropagation(); e.preventDefault(); - PanelManager.toggle(tag, blockUid, parentEl, onloadArgs); + panelManager.toggle({ tag, blockUid, onloadArgs }); + setIsPanelOpen(panelManager.isOpen(tag)); }, - [tag, blockUid, parentEl, onloadArgs], + [tag, blockUid, onloadArgs], ); return ( diff --git a/apps/roam/src/components/DiscourseSuggestionsPanel.tsx b/apps/roam/src/components/DiscourseSuggestionsPanel.tsx index 8c5513225..a7398a4ed 100644 --- a/apps/roam/src/components/DiscourseSuggestionsPanel.tsx +++ b/apps/roam/src/components/DiscourseSuggestionsPanel.tsx @@ -6,7 +6,7 @@ import { Navbar, Collapse, } from "@blueprintjs/core"; -import React, { useMemo, useState, useCallback } from "react"; +import React, { useState, useCallback } from "react"; import SuggestionsBody from "./SuggestionsBody"; import { useDiscourseData } from "~/utils/useDiscourseData"; @@ -20,11 +20,10 @@ export const DiscourseSuggestionsPanel = ({ onClose: () => void; }) => { const [isOpen, setIsOpen] = useState(true); - const { results } = useDiscourseData(tag); + const { results, loading } = useDiscourseData(tag); const toggleHighlight = useCallback( (on: boolean) => { - console.log("toggleHighlight", blockUid, on); document .querySelectorAll(`[data-dg-block-uid="${blockUid}"]`) .forEach((el) => el.classList.toggle("dg-highlight", on)); @@ -55,7 +54,6 @@ export const DiscourseSuggestionsPanel = ({ alignItems: "center", }} > - {/* Left-aligned group for panel heading */} - - {/* Right-aligned group for action buttons */} diff --git a/apps/roam/src/components/PanelManager.tsx b/apps/roam/src/components/PanelManager.tsx index 4c9feb754..174cb712b 100644 --- a/apps/roam/src/components/PanelManager.tsx +++ b/apps/roam/src/components/PanelManager.tsx @@ -1,92 +1,115 @@ import React from "react"; import ReactDOM from "react-dom"; +import { Navbar, Alignment, Button } from "@blueprintjs/core"; import { OnloadArgs } from "roamjs-components/types/native"; import ExtensionApiContextProvider from "roamjs-components/components/ExtensionApiContext"; import { DiscourseSuggestionsPanel } from "./DiscourseSuggestionsPanel"; -// Constants const PANEL_ROOT_ID = "discourse-graph-suggestions-root"; const PANELS_CONTAINER_ID = "discourse-graph-panels-container"; const ARTICLE_WRAPPER_SELECTOR = ".rm-article-wrapper"; const MINIMIZED_BAR_ID = "discourse-suggestions-minimized"; +const SPACING_PREFIX = "rm-spacing--"; +const initialSpacingByWrapper = new WeakMap(); + +const STYLES = { + minimizedBar: ` + display: flex; align-items: center; gap: 4px; padding: 6px 8px; + background: #fff; border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + margin: 8px; width: fit-content; + `, + panelsContainer: ` + display: flex; flex-direction: column; flex: 1 1 auto; gap: 8px; + padding: 8px; background-color: #f5f5f5; overflow-y: auto; + `, + header: ` + flex: 0 0 auto; padding: 6px 8px; background-color: #fff; + border-radius: 4px 4px 0 0; margin-bottom: 0; font-weight: 600; + font-size: 13px; box-shadow: 0 1px 3px rgba(0,0,0,0.08); + display: flex; justify-content: space-between; align-items: center; + `, + headerButtons: `display: flex; gap: 4px; align-items: center;`, + button: `cursor: pointer; border: none; background: transparent; padding: 2px 6px;`, + minimizeButton: `cursor: pointer; border: none; background: transparent; padding: 2px 6px; font-size: 16px;`, + panelElement: ` + flex: 0 0 auto; margin-bottom: 8px; background-color: #fff; + border-radius: 4px; box-shadow: 0 1px 3px rgba(0,0,0,0.1); + `, +} as const; -// Track open panels globally type PanelState = { blockUid: string; element: HTMLElement; onloadArgs: OnloadArgs; }; -const openPanels = new Map(); -let isContainerMinimized = false; -let articleWrapperObserver: MutationObserver | null = null; -let navigationObserver: MutationObserver | null = null; -// Initialize observer for article wrapper changes -const initializeObserver = (mainContent: HTMLElement) => { - if (articleWrapperObserver) { - return; +const getSpacingClass = (element: HTMLElement): string | null => { + for (const className of Array.from(element.classList)) { + if (className.startsWith(SPACING_PREFIX)) return className; } - articleWrapperObserver = new MutationObserver(() => { - const root = document.getElementById(PANEL_ROOT_ID); - if (root && root.style.display !== "none" && !isContainerMinimized) { - if (!mainContent.classList.contains("rm-spacing--full")) { - mainContent.classList.add("rm-spacing--full"); - mainContent.classList.remove("rm-spacing--small"); - } - } - }); - articleWrapperObserver.observe(mainContent, { - attributes: true, - attributeFilter: ["class"], - }); + return null; }; -// Initialize observer for navigation changes -const initializeNavigationObserver = () => { - if (navigationObserver) { - return; +const setSpacingClass = ( + element: HTMLElement, + spacingClass: string | null, +): void => { + for (const className of Array.from(element.classList)) { + if (className.startsWith(SPACING_PREFIX)) + element.classList.remove(className); } + if (spacingClass) element.classList.add(spacingClass); +}; - const roamApp = document.querySelector(".roam-app"); - if (!roamApp) return; - - navigationObserver = new MutationObserver((mutations) => { - // Check if we still have our panel root - const panelRoot = document.getElementById(PANEL_ROOT_ID); - const roamBodyMain = getRoamBodyMain(); - - // If we have open panels but the infrastructure is gone, recreate it - if (openPanels.size > 0 && (!panelRoot || !panelRoot.parentElement)) { - restorePanelInfrastructure(); - } - - // Ensure split view is maintained if panels are open - if (openPanels.size > 0 && roamBodyMain && panelRoot) { - const articleWrapper = getArticleWrapper(roamBodyMain); - if (articleWrapper && roamBodyMain.dataset.isSplit !== "true") { - setupSplitView(roamBodyMain, articleWrapper, panelRoot); - } - } - }); +const SidebarHeader = ({ + onMinimize, + onCloseAll, +}: { + onMinimize: () => void; + onCloseAll: () => void; +}) => ( + + + + Suggested Discourse nodes + + + + } @@ -120,15 +143,7 @@ const DiscourseContextOverlay = ({ ); }; -const Wrapper = ({ - parent, - tag, - onloadArgs, -}: { - parent: HTMLElement; - tag: string; - onloadArgs: OnloadArgs; -}) => { +const Wrapper = ({ parent, tag }: { parent: HTMLElement; tag: string }) => { const id = useMemo(() => nanoid(), []); const { inViewport } = useInViewport( { current: parent }, @@ -137,12 +152,7 @@ const Wrapper = ({ {}, ); return inViewport ? ( - + ) : (