diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index a72d6c0..89f6580 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -9,6 +9,7 @@ import { import '@xyflow/react/dist/style.css'; import CustomNode, { NodeContext } from './CustomNode'; +import HelpPanelManager from './HelpPanelManager'; import * as api from './api'; import { pickNativeDirectorySelection, pickNativeFileSelection } from './nativePicker'; import { toBlob } from 'html-to-image'; @@ -854,6 +855,8 @@ function Flow() { const [contextMenu, setContextMenu] = useState(null); const [isCanvasRightZooming, setIsCanvasRightZooming] = useState(false); const [executingNodeId, setExecutingNodeId] = useState(null); + const [helpTabs, setHelpTabs] = useState([]); + const [activeHelpTab, setActiveHelpTab] = useState(null); const flowContainerRef = useRef(null); const panTimerRef = useRef(null); @@ -1647,6 +1650,11 @@ function Flow() { const addNode = useCallback((className, def) => { if (!contextMenu) return; + if (className === 'TextNote') { + openJournalTab(); + setContextMenu(null); + return; + } const position = reactFlow.screenToFlowPosition({ x: contextMenu.x, y: contextMenu.y, @@ -1745,7 +1753,7 @@ function Flow() { setContextMenu(null); scheduleAutoRun(); - }, [contextMenu, reactFlow, refreshFolderNodeOutputs, refreshLoadNodeOutputs, setNodes, setEdges]); // scheduleAutoRun is stable (no deps) + }, [contextMenu, reactFlow, refreshFolderNodeOutputs, refreshLoadNodeOutputs, setNodes, setEdges]); // scheduleAutoRun stable; openJournalTab stable ([] deps) // ── Toolbar actions ───────────────────────────────────────────────── @@ -1921,6 +1929,45 @@ function Flow() { })); }, [setNodes]); + const openHelp = useCallback(async (label) => { + setHelpTabs((prev) => { + if (prev.find((t) => t.label === label)) return prev; + return [...prev, { label, content: null }]; + }); + setActiveHelpTab(label); + const text = await api.getNodeDoc(label); + setHelpTabs((prev) => + prev.map((t) => + t.label === label + ? { ...t, content: text || '*No documentation available for this node.*' } + : t, + ), + ); + }, []); + + const closeHelpTab = useCallback((label) => { + setHelpTabs((prev) => { + const next = prev.filter((t) => t.label !== label); + setActiveHelpTab((cur) => { + if (cur !== label) return cur; + return next.length > 0 ? next[next.length - 1].label : null; + }); + return next; + }); + }, []); + + const openJournalTab = useCallback(() => { + setHelpTabs((prev) => { + if (prev.find((t) => t.label === 'Journal')) return prev; + return [...prev, { label: 'Journal', type: 'journal', content: '' }]; + }); + setActiveHelpTab('Journal'); + }, []); + + const updateTabContent = useCallback((label, content) => { + setHelpTabs((prev) => prev.map((t) => t.label === label ? { ...t, content } : t)); + }, []); + const contextValue = useMemo(() => ({ onWidgetChange, onRuntimeValuesChange, @@ -1931,7 +1978,8 @@ function Flow() { onRenameGroup: renameGroup, onUngroup: ungroupGroup, executingNodeId, - }), [onRuntimeValuesChange, onWidgetChange, openFileBrowser, onManualTrigger, renameGroup, resizeGroup, toggleGroupCollapse, ungroupGroup, executingNodeId]); + openHelp, + }), [onRuntimeValuesChange, onWidgetChange, openFileBrowser, onManualTrigger, renameGroup, resizeGroup, toggleGroupCollapse, ungroupGroup, executingNodeId, openHelp]); const clearGraph = useCallback(() => { setNodes([]); @@ -2949,6 +2997,13 @@ function Flow() { + ); } diff --git a/frontend/src/CustomNode.jsx b/frontend/src/CustomNode.jsx index bbed599..067d738 100644 --- a/frontend/src/CustomNode.jsx +++ b/frontend/src/CustomNode.jsx @@ -1,8 +1,6 @@ import React, { useContext, useRef, useCallback, useState, useEffect, memo, lazy, Suspense } from 'react'; -import ReactDOM from 'react-dom'; import { Handle, NodeResizeControl, Position, useStore } from '@xyflow/react'; import { marked } from 'marked'; -import { getNodeDoc } from './api'; import LinePlotOverlay from './LinePlotOverlay'; marked.use({ breaks: true, gfm: true }); @@ -978,47 +976,10 @@ function NodeTable({ rows }) { ); } -// ── Node help panel (portal) ────────────────────────────────────────── - -function NodeHelpPanel({ title, content, onClose }) { - useEffect(() => { - const handler = (e) => { if (e.key === 'Escape') onClose(); }; - document.addEventListener('keydown', handler); - return () => document.removeEventListener('keydown', handler); - }, [onClose]); - - return ReactDOM.createPortal( -
-
- {title} - -
-
-
, - document.body, - ); -} - // ── CustomNode component ────────────────────────────────────────────── function CustomNode({ id, data }) { const ctx = useContext(NodeContext); - const [helpOpen, setHelpOpen] = useState(false); - const [helpContent, setHelpContent] = useState(null); - - const onHelpClick = useCallback(async (e) => { - e.stopPropagation(); - if (helpOpen) { setHelpOpen(false); return; } - setHelpOpen(true); - if (helpContent === null) { - const text = await getNodeDoc(data.label); - setHelpContent(text || '*No documentation available for this node.*'); - } - }, [helpOpen, helpContent, data.label]); if (data.className === 'Group') { return ; @@ -1225,7 +1186,7 @@ function CustomNode({ id, data }) {
{data.label} - +
{headerMeta && {headerMeta}}
@@ -1574,13 +1535,6 @@ function CustomNode({ id, data }) { )}
- {helpOpen && ( - setHelpOpen(false)} - /> - )} ); } diff --git a/frontend/src/HelpPanelManager.jsx b/frontend/src/HelpPanelManager.jsx new file mode 100644 index 0000000..6f71930 --- /dev/null +++ b/frontend/src/HelpPanelManager.jsx @@ -0,0 +1,117 @@ +import React, { useEffect, useState } from 'react'; +import ReactDOM from 'react-dom'; +import { marked } from 'marked'; + +function JournalTab({ content, onChange }) { + const [isEditing, setIsEditing] = useState(false); + const renderedHtml = content?.trim() ? marked.parse(content) : ''; + + return ( +
+
+ + {isEditing && ( + Ctrl+Enter to preview + )} +
+ {isEditing ? ( +