diff --git a/src/apps/beam/AppBeam.tsx b/src/apps/beam/AppBeam.tsx
index 9eeec93b1..58e477091 100644
--- a/src/apps/beam/AppBeam.tsx
+++ b/src/apps/beam/AppBeam.tsx
@@ -8,7 +8,7 @@ import { BeamView } from '~/modules/beam/BeamView';
import { createBeamVanillaStore } from '~/modules/beam/store-beam-vanilla';
import { useModelsStore } from '~/modules/llms/store-llms';
-import { OptimaPortalIn } from '~/common/layout/optima/portals/OptimaPortalIn';
+import { OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { createDConversation, DConversation } from '~/common/stores/chat/chat.conversation';
import { createDMessageTextContent, DMessage } from '~/common/stores/chat/chat.message';
import { useIsMobile } from '~/common/components/useMatchMedia';
@@ -76,7 +76,7 @@ export function AppBeam() {
return <>
- {toolbarItems}
+ {toolbarItems}
diff --git a/src/apps/call/Telephone.tsx b/src/apps/call/Telephone.tsx
index 195de35cd..1d1d44687 100644
--- a/src/apps/call/Telephone.tsx
+++ b/src/apps/call/Telephone.tsx
@@ -20,7 +20,7 @@ import { useElevenLabsVoiceDropdown } from '~/modules/elevenlabs/useElevenLabsVo
import { AudioPlayer } from '~/common/util/audio/AudioPlayer';
import { Link } from '~/common/components/Link';
-import { OptimaPortalIn } from '~/common/layout/optima/portals/OptimaPortalIn';
+import { OptimaToolbarIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { SpeechResult, useSpeechRecognition } from '~/common/components/useSpeechRecognition';
import { conversationTitle } from '~/common/stores/chat/chat.conversation';
import { createDMessageTextContent, DMessage, messageFragmentsReduceText, messageSingleTextOrThrow } from '~/common/stores/chat/chat.message';
@@ -307,7 +307,7 @@ export function Telephone(props: {
return <>
- {chatLLMDropdown}
+ {chatLLMDropdown}
-
- {drawerContent}
- {focusedBarContent}
+ {drawerContent}
+ {focusedBarContent}
- {drawSectionDropdown}
+ {drawSectionDropdown}
{drawSection === 'create' ? (
{capitalizeFirstLetter(pageTitle)} ยท {Brand.Title.Base} ๐
- {drawerContent}
+ {drawerContent}
{isListPage
?
diff --git a/src/apps/personas/AppPersonas.tsx b/src/apps/personas/AppPersonas.tsx
index 3f5119cda..545677564 100644
--- a/src/apps/personas/AppPersonas.tsx
+++ b/src/apps/personas/AppPersonas.tsx
@@ -2,7 +2,7 @@ import * as React from 'react';
import { Box, Container, ListDivider, Typography } from '@mui/joy';
-import { OptimaPortalIn } from '~/common/layout/optima/portals/OptimaPortalIn';
+import { OptimaDrawerIn } from '~/common/layout/optima/portals/OptimaPortalsIn';
import { Creator } from './creator/Creator';
import { CreatorDrawer } from './creator/CreatorDrawer';
@@ -27,7 +27,7 @@ export function AppPersonas() {
}, [selectedSimplePersonaId]);
return <>
- {drawerContent}
+ {drawerContent}
({
export function DesktopDrawer(props: { component: React.ElementType, currentApp?: NavItemApp }) {
// state
- const drawerPortalRef = useOptimaPortalOut('optima-portal-drawer', 'DesktopDrawer');
+ const drawerPortalRef = useOptimaPortalOutRef('optima-portal-drawer', 'DesktopDrawer');
// external state
const { isDrawerOpen, closeDrawer, openDrawer } = useOptimaDrawers();
diff --git a/src/common/layout/optima/MobileDrawer.tsx b/src/common/layout/optima/MobileDrawer.tsx
index a31e88d1c..ba0665986 100644
--- a/src/common/layout/optima/MobileDrawer.tsx
+++ b/src/common/layout/optima/MobileDrawer.tsx
@@ -5,11 +5,11 @@ import { Box, Drawer } from '@mui/joy';
import type { NavItemApp } from '~/common/app.nav';
import { useOptimaDrawers } from './useOptimaDrawers';
-import { useOptimaPortalOut } from './portals/useOptimaPortalOut';
+import { useOptimaPortalOutRef } from './portals/useOptimaPortalOutRef';
function DrawerContentPortal() {
- const drawerPortalRef = useOptimaPortalOut('optima-portal-drawer', 'MobileDrawer');
+ const drawerPortalRef = useOptimaPortalOutRef('optima-portal-drawer', 'MobileDrawer');
return (
@@ -51,7 +51,7 @@ const centerItemsContainerSx: SxProps = {
};
function CenterItemsPortal() {
- const portalToolbarRef = useOptimaPortalOut('optima-portal-toolbar', 'PageBar.CenterItemsContainer');
+ const portalToolbarRef = useOptimaPortalOutRef('optima-portal-toolbar', 'PageBar.CenterItemsContainer');
return (
{/* TODO */}
diff --git a/src/common/layout/optima/portals/OptimaPortalIn.tsx b/src/common/layout/optima/portals/OptimaPortalIn.tsx
deleted file mode 100644
index c18b47995..000000000
--- a/src/common/layout/optima/portals/OptimaPortalIn.tsx
+++ /dev/null
@@ -1,18 +0,0 @@
-import * as React from 'react';
-import { createPortal } from 'react-dom';
-
-import { OptimaPortalId, usePortalElement } from './store-optima-portals';
-
-
-export function OptimaPortalIn(props: {
- targetPortalId: OptimaPortalId;
- children: React.ReactNode;
-}) {
-
- // react to the portal being made available
- const portalElement = usePortalElement(props.targetPortalId);
- if (!portalElement)
- return null;
-
- return createPortal(props.children, portalElement);
-}
diff --git a/src/common/layout/optima/portals/OptimaPortalsIn.tsx b/src/common/layout/optima/portals/OptimaPortalsIn.tsx
new file mode 100644
index 000000000..97f4c4fe8
--- /dev/null
+++ b/src/common/layout/optima/portals/OptimaPortalsIn.tsx
@@ -0,0 +1,34 @@
+import * as React from 'react';
+import { createPortal } from 'react-dom';
+
+import { OptimaPortalId, useOptimaPortalsStore } from './store-optima-portals';
+
+
+export function OptimaDrawerIn(props: { children: React.ReactNode }) {
+ const portalElement = _useOptimaPortalTargetElement('optima-portal-drawer');
+ return portalElement ? createPortal(props.children, portalElement) : null;
+}
+
+export function OptimaToolbarIn(props: { children: React.ReactNode }) {
+ const portalElement = _useOptimaPortalTargetElement('optima-portal-toolbar');
+ return portalElement ? createPortal(props.children, portalElement) : null;
+}
+
+
+/**
+ * Hook to get the target element for a portal.
+ */
+function _useOptimaPortalTargetElement(targetPortalId: OptimaPortalId) {
+ // get the output element
+ const targetPortalEl = useOptimaPortalsStore(state => state.portals[targetPortalId]?.element ?? null);
+
+ // increment/decrement input count
+ React.useEffect(() => {
+ const { incrementInputs, decrementInputs } = useOptimaPortalsStore.getState();
+ incrementInputs(targetPortalId);
+ return () => decrementInputs(targetPortalId);
+ }, [targetPortalId]);
+
+ // return the output element
+ return targetPortalEl;
+}
diff --git a/src/common/layout/optima/portals/store-optima-portals.ts b/src/common/layout/optima/portals/store-optima-portals.ts
index ae72431b0..b2d213f50 100644
--- a/src/common/layout/optima/portals/store-optima-portals.ts
+++ b/src/common/layout/optima/portals/store-optima-portals.ts
@@ -1,3 +1,4 @@
+import * as React from 'react';
import { create } from 'zustand';
@@ -7,51 +8,71 @@ const DEBUG_OPTIMA_PORTALS = false;
export type OptimaPortalId =
| 'optima-portal-drawer'
- // | 'optima-portal-properties'
- | 'optima-portal-toolbar'
- ;
+ | 'optima-portal-toolbar';
-interface PortalState {
- portalElements: Map;
+
+interface OptimaPortalState {
+ portals: Record;
}
-interface PortalActions {
- addPortal: (id: OptimaPortalId, element: HTMLElement) => void;
- removePortal: (id: OptimaPortalId) => void;
+interface OptimaPortalActions {
+
+ // store output elements
+ setElement: (id: OptimaPortalId, element: HTMLElement | null) => void;
+
+ // reference counting
+ incrementInputs: (id: OptimaPortalId) => void;
+ decrementInputs: (id: OptimaPortalId) => void;
+
}
-type PortalStore = PortalState & PortalActions;
-
-const useOptimaPortalsStore = create((set, get) => ({
+export const useOptimaPortalsStore = create((_set) => ({
// init state
- portalElements: new Map(),
+ portals: {
+ 'optima-portal-drawer': { element: null, inputs: 0 },
+ 'optima-portal-toolbar': { element: null, inputs: 0 },
+ },
// actions
- addPortal: (id, element) => set((state) => {
- const newPortals = new Map(state.portalElements);
- newPortals.set(id, element);
+
+ setElement: (id, element) => _set((state) => {
if (DEBUG_OPTIMA_PORTALS)
- console.log(' > store.addPortal', id, !!element);
- return { portalElements: newPortals };
+ console.log(`${element ? 'Set' : 'Remove'} portal element`, id);
+ return {
+ portals: {
+ ...state.portals,
+ [id]: { ...state.portals[id], element: element },
+ },
+ };
}),
- removePortal: (id) => set((state) => {
- const newPortals = new Map(state.portalElements);
- newPortals.delete(id);
+ incrementInputs: (id) => _set((state) => {
+ const newInputs = state.portals[id].inputs + 1;
if (DEBUG_OPTIMA_PORTALS)
- console.log(' < store.removePortal', id);
- return { portalElements: newPortals };
+ console.log(' + store.incrementInputs', id, newInputs);
+ return {
+ portals: {
+ ...state.portals,
+ [id]: { ...state.portals[id], inputs: newInputs },
+ },
+ };
+ }),
+
+ decrementInputs: (id) => _set((state) => {
+ const newInputs = Math.max(0, state.portals[id].inputs - 1);
+ if (DEBUG_OPTIMA_PORTALS)
+ console.log(' - store.decrementInputs', id, newInputs);
+ return {
+ portals: {
+ ...state.portals,
+ [id]: { ...state.portals[id], inputs: newInputs },
+ },
+ };
}),
}));
-
-
-export function optimaPortalsActions(): PortalActions {
- return useOptimaPortalsStore.getState();
-}
-
-export function usePortalElement(id: OptimaPortalId) {
- return useOptimaPortalsStore((state) => state.portalElements.get(id) || null);
-}
diff --git a/src/common/layout/optima/portals/useOptimaPortalOut.tsx b/src/common/layout/optima/portals/useOptimaPortalOut.tsx
deleted file mode 100644
index 765ac4e7f..000000000
--- a/src/common/layout/optima/portals/useOptimaPortalOut.tsx
+++ /dev/null
@@ -1,45 +0,0 @@
-import * as React from 'react';
-
-import { OptimaPortalId, optimaPortalsActions } from './store-optima-portals';
-
-
-/**
- * Note: this hook assumes that the ref is created by when useLayoutEffect is called,
- * and will warn otherwise.
- */
-export function useOptimaPortalOut(portalTargetId: OptimaPortalId, debugCallerName: string) {
-
- // state
- const ref = React.useRef(null);
-
- React.useLayoutEffect(() => {
- const { addPortal, removePortal } = optimaPortalsActions();
- if (!ref.current) {
- console.warn(`useOptimaPortalOut: ref.current is null for type ${portalTargetId} (called by ${debugCallerName})`);
- } else {
- addPortal(portalTargetId, ref.current);
- }
- return () => removePortal(portalTargetId);
- }, [debugCallerName, portalTargetId]);
-
- return ref;
-}
-
-/*
-// This version will add the portal only when really getting the ref
-export function useOptimaPortalOut(portalTargetId: OptimaPortalId, debugCallerName: string) {
-
- const setRef = React.useCallback((node: HTMLElement | null) => {
- const { addPortal, removePortal } = optimaPortalsActions();
- console.log('useOptimaPortalOut.setRef', portalTargetId, node);
- if (node) {
- console.log(' - useOptimaPortalOut call AddPortal', portalTargetId);
- addPortal(portalTargetId, node);
- } else {
- removePortal(portalTargetId);
- }
- }, [portalTargetId]);
-
- return setRef;
-}
-*/
\ No newline at end of file
diff --git a/src/common/layout/optima/portals/useOptimaPortalOutRef.ts b/src/common/layout/optima/portals/useOptimaPortalOutRef.ts
new file mode 100644
index 000000000..64bb108da
--- /dev/null
+++ b/src/common/layout/optima/portals/useOptimaPortalOutRef.ts
@@ -0,0 +1,33 @@
+import * as React from 'react';
+
+import { OptimaPortalId, useOptimaPortalsStore } from './store-optima-portals';
+
+
+/**
+ * Defines a React output portal for a given target id. Will return a ref that
+ * must be attached to the target element.
+ *
+ * Note: this hook assumes that the ref is created by when useLayoutEffect is called,
+ * and will warn otherwise.
+ *
+ * If the ref is created after the layout effect, the portal will not be added. In
+ * that case, consider returning a Callback instead, with:
+ * `const setRef = React.useCallback((node: HTMLElement | null) => { ... }, [portalTargetId]);`
+ */
+export function useOptimaPortalOutRef(portalTargetId: OptimaPortalId, debugCallerName: string) {
+
+ // state
+ const ref = React.useRef(null);
+
+ React.useLayoutEffect(() => {
+ const { setElement } = useOptimaPortalsStore.getState();
+ if (!ref.current) {
+ console.warn(`useOptimaPortalOut: ref.current is null for type ${portalTargetId} (called by ${debugCallerName})`);
+ } else {
+ setElement(portalTargetId, ref.current);
+ }
+ return () => setElement(portalTargetId, null);
+ }, [debugCallerName, portalTargetId]);
+
+ return ref;
+}