diff --git a/docs/APP_REFACTORING_DESIGN.md b/docs/APP_REFACTORING_DESIGN.md index 92f66f1..fabd800 100644 --- a/docs/APP_REFACTORING_DESIGN.md +++ b/docs/APP_REFACTORING_DESIGN.md @@ -86,8 +86,8 @@ To refactor `app.tsx` safely and maintain continuous code correctness, we will m * [x] **Step 1.4:** Modify `src/menu/top_bar.tsx` to make chart-specific props and event handlers optional (e.g. `data`, `allowAllRelativesChart`, `allowPrintAndDownload`, `eventHandlers`, etc.), preparing the component for rendering on the landing screen without dummy properties. #### Phase 2: WebMCP Bridge Extraction -* [ ] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP registration and synchronization effects. -* [ ] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in `src/app.tsx` with a single call to the custom `useWebMcpBridge` hook. Verify using `npm test`. Note: During Phase 4, this hook call will be moved from `src/app.tsx` into `src/pages/view_page.tsx` where the layout state is relocated. +* [x] **Step 2.1:** Create `src/hooks/use_webmcp_bridge.ts` wrapping WebMCP registration and synchronization effects. +* [x] **Step 2.2:** Replace the inline WebMCP logic and `useEffect` blocks in `src/app.tsx` with a single call to the custom `useWebMcpBridge` hook. Verify using `npm test`. Note: During Phase 4, this hook call will be moved from `src/app.tsx` into `src/pages/view_page.tsx` where the layout state is relocated. #### Phase 3: Shifting to URL as Single Source of Truth We will gradually eliminate React state variables in `src/app.tsx` and derive them (including settings like `standalone`, `showWikiTreeMenus`, and `freezeAnimation`) directly from the `useLocation()` search query parameters on render: diff --git a/src/app.tsx b/src/app.tsx index 40b097c..7fb17b0 100644 --- a/src/app.tsx +++ b/src/app.tsx @@ -43,6 +43,7 @@ import { WikiTreeSourceSpec, } from './datasource/wikitree'; import {DonatsoChart} from './donatso-chart'; +import {useWebMcpBridge} from './hooks/use_webmcp_bridge'; import {Intro} from './intro'; import {GoogleAuthModal} from './menu/google_auth_modal'; import {TopBar} from './menu/top_bar'; @@ -64,7 +65,6 @@ import { getStaticUrl, getUrlForArgs, } from './util/url_args'; -import {WebMcpBridge} from './webmcp'; const staticUrl = getStaticUrl(); @@ -108,8 +108,7 @@ export function App() { const [freezeAnimation, setFreezeAnimation] = useState(false); /** Configuration settings for chart display options (e.g. colors, hiding IDs). */ const [config, setConfig] = useState(DEFALUT_CONFIG); - /** MCP bridge to communicate with external tools or servers (Model Context Protocol). */ - const [mcpBridge] = useState(() => new WebMcpBridge()); + /** Controls the visibility of the Google Drive OAuth permission modal. */ const [showAuthModal, setShowAuthModal] = useState(false); /** Stores the file ID that failed to load from Google Drive due to authorization errors. */ @@ -456,13 +455,6 @@ export function App() { updateDisplay, ]); - useEffect(() => { - mcpBridge.registerTools(); - return () => { - mcpBridge.unregisterTools(); - }; - }, [mcpBridge]); - // Clean up object URLs created for uploaded images/files when the dataset // changes or the app unmounts to prevent memory leaks. useEffect(() => { @@ -471,19 +463,7 @@ export function App() { }; }, [data]); - useEffect(() => { - mcpBridge.setData(data || null); - }, [data, mcpBridge]); - - useEffect(() => { - mcpBridge.setDetailIndi(detailIndi || null); - }, [detailIndi, mcpBridge]); - - useEffect(() => { - mcpBridge.setSetSelectionCallback((id: string) => { - onSelection({id, generation: 0}); - }); - }, [mcpBridge, location]); + useWebMcpBridge(data, detailIndi, onSelection); function updateUrl( args: Record, diff --git a/src/hooks/use_webmcp_bridge.ts b/src/hooks/use_webmcp_bridge.ts new file mode 100644 index 0000000..2616340 --- /dev/null +++ b/src/hooks/use_webmcp_bridge.ts @@ -0,0 +1,49 @@ +import {useEffect, useRef, useState} from 'react'; +import {IndiInfo} from 'topola'; +import {TopolaData} from '../util/gedcom_util'; +import {WebMcpBridge} from '../webmcp'; + +/** + * Custom hook to manage the lifecycle and synchronization of the WebMCP bridge. + * It instantiates the WebMcpBridge and coordinates tool registration, data updates, + * detail selection updates, and selection callbacks. + */ +export function useWebMcpBridge( + data: TopolaData | undefined | null, + detailIndi: string | undefined | null, + onSelection: (selection: IndiInfo) => void, +) { + const [mcpBridge] = useState(() => new WebMcpBridge()); + + // Store the onSelection callback in a ref to prevent recreating the selection callback effect + // when onSelection changes. + const onSelectionRef = useRef(onSelection); + useEffect(() => { + onSelectionRef.current = onSelection; + }, [onSelection]); + + // Handle registration and cleanup of WebMCP tools. + useEffect(() => { + mcpBridge.registerTools(); + return () => { + mcpBridge.unregisterTools(); + }; + }, [mcpBridge]); + + // Synchronize the active dataset with the bridge. + useEffect(() => { + mcpBridge.setData(data || null); + }, [data, mcpBridge]); + + // Synchronize the currently selected individual for details display with the bridge. + useEffect(() => { + mcpBridge.setDetailIndi(detailIndi || null); + }, [detailIndi, mcpBridge]); + + // Register the viewport navigation callback with the bridge. + useEffect(() => { + mcpBridge.setSetSelectionCallback((id: string) => { + onSelectionRef.current({id, generation: 0}); + }); + }, [mcpBridge]); +}