diff --git a/package-lock.json b/package-lock.json index 5cd67c295..d2bec4cbb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -38,6 +38,7 @@ "react-dom": "^18.2.0", "react-katex": "^3.0.1", "react-markdown": "^9.0.1", + "react-resizable-panels": "^1.0.5", "react-timeago": "^7.2.0", "remark-gfm": "^4.0.0", "superjson": "^2.2.1", @@ -5874,6 +5875,15 @@ "react": ">=18" } }, + "node_modules/react-resizable-panels": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/react-resizable-panels/-/react-resizable-panels-1.0.5.tgz", + "integrity": "sha512-OP0whNQCko+f4BgoptGaeIc7StBRyeMeJ+8r/7rXACBDf9W5EcMWuM32hfqPDMenS2HFy/eZVi/r8XqK+ZIEag==", + "peerDependencies": { + "react": "^16.14.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.14.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-ssr-prepass": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/react-ssr-prepass/-/react-ssr-prepass-1.5.0.tgz", diff --git a/package.json b/package.json index 432bf6fe1..9a0af6088 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "react-dom": "^18.2.0", "react-katex": "^3.0.1", "react-markdown": "^9.0.1", + "react-resizable-panels": "^1.0.5", "react-timeago": "^7.2.0", "remark-gfm": "^4.0.0", "superjson": "^2.2.1", diff --git a/src/apps/chat/AppChat.tsx b/src/apps/chat/AppChat.tsx index 79b745f12..57d42b0c9 100644 --- a/src/apps/chat/AppChat.tsx +++ b/src/apps/chat/AppChat.tsx @@ -1,7 +1,8 @@ import * as React from 'react'; - -import { Box } from '@mui/joy'; import ForkRightIcon from '@mui/icons-material/ForkRight'; +import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels'; + +import { Box, useTheme } from '@mui/joy'; import { CmdRunBrowse } from '~/modules/browse/browse.client'; import { CmdRunReact } from '~/modules/aifn/react/react'; @@ -63,6 +64,8 @@ export function AppChat() { const composerTextAreaRef = React.useRef(null); // external state + const theme = useTheme(); + const { openLlmOptions } = useOptimaLayout(); const { chatLLM } = useChatLLM(); @@ -74,6 +77,7 @@ export function AppChat() { openConversationInFocusedPane, openConversationInSplitPane, setFocusedPaneIndex, + removePaneIndex, } = usePanesManager(); const { @@ -97,10 +101,6 @@ export function AppChat() { const chatPaneIDs = chatPanes.length > 0 ? chatPanes.map(pane => pane.conversationId) : [null]; - const setActivePaneIndex = React.useCallback((idx: number) => { - setFocusedPaneIndex(idx); - }, [setFocusedPaneIndex]); - const setFocusedConversationId = React.useCallback((conversationId: DConversationId | null) => { conversationId && openConversationInFocusedPane(conversationId); }, [openConversationInFocusedPane]); @@ -382,18 +382,28 @@ export function AppChat() { return <> - + - {chatPaneIDs.map((_conversationId, idx) => ( - setActivePaneIndex(idx)} sx={{ - flexGrow: 1, flexBasis: 1, - display: 'flex', flexDirection: 'column', - overflow: 'clip', - }}> + {chatPaneIDs.map((_conversationId, idx, panels) => + + 0 ? Math.round(100 / panels.length) : undefined} + minSize={20} + onClick={() => setFocusedPaneIndex(idx)} + onCollapse={() => setTimeout(() => removePaneIndex(idx), 50)} + style={{ + // allows the content to be scrolled (all browsers) + overflowY: 'auto', + // border only for active pane (if two or more panes) + ...(chatPaneIDs.length < 2 ? {} + : (_conversationId === focusedConversationId) + ? { border: `2px solid ${theme.palette.primary.solidBg}` } + : { border: `2px solid ${theme.palette.background.level1}` }), + }} + > - - ))} - + + + {/* Panel Separators & Resizers */} + {idx < panels.length - 1 && ( + + + + )} + + )} + + void; navigateHistoryInFocusedPane: (direction: 'back' | 'forward') => boolean; setFocusedPaneIndex: (paneIndex: number) => void; - splitChatPane: (numberOfPanes: number) => void; - unsplitChatPane: (paneIndexToKeep: number) => void; + removePaneIndex: (paneIndex: number) => void; onConversationsChanged: (conversationIds: DConversationId[]) => void; } @@ -169,22 +168,20 @@ const useAppChatPanesStore = create()(persist( }; }), - splitChatPane: (numberOfPanes: number) => { - const { chatPanes, chatPaneFocusIndex } = _get(); - const focusedPane = (chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex] : null) ?? createPane(); + removePaneIndex: (paneIndex: number) => + _set(state => { + const { chatPanes, chatPaneFocusIndex } = state; + if (paneIndex < 0 || paneIndex >= chatPanes.length) + return state; - _set({ - chatPanes: Array.from({ length: numberOfPanes }, () => ({ ...focusedPane })), - chatPaneFocusIndex: 0, - }); - }, - - unsplitChatPane: (paneIndexToKeep: number) => - _set(state => ({ - chatPanes: [state.chatPanes[paneIndexToKeep] || createPane()], - chatPaneFocusIndex: 0, - })), + const newPanes = chatPanes.toSpliced(paneIndex, 1); + // when a pane is removed, focus the pane 0, or null if no panes remain + return { + chatPanes: newPanes, + chatPaneFocusIndex: newPanes.length ? 0 : null, + }; + }), /** * This function is vital, as is invoked when the conversationId[] changes in the global chats store. @@ -258,6 +255,7 @@ export function usePanesManager() { onConversationsChanged, openConversationInFocusedPane, openConversationInSplitPane, + removePaneIndex, setFocusedPaneIndex, } = state; const focusedConversationId = chatPaneFocusIndex !== null ? chatPanes[chatPaneFocusIndex]?.conversationId ?? null : null; @@ -268,6 +266,7 @@ export function usePanesManager() { onConversationsChanged, openConversationInFocusedPane, openConversationInSplitPane, + removePaneIndex, setFocusedPaneIndex, }; }, shallow); diff --git a/src/common/layout/optima/OptimaLayout.tsx b/src/common/layout/optima/OptimaLayout.tsx index 1edb59f7d..97fef3f37 100644 --- a/src/common/layout/optima/OptimaLayout.tsx +++ b/src/common/layout/optima/OptimaLayout.tsx @@ -7,6 +7,7 @@ import { SettingsModal } from '../../../apps/settings-modal/SettingsModal'; import { ShortcutsModal } from '../../../apps/settings-modal/ShortcutsModal'; import { isPwa } from '~/common/util/pwaUtils'; +import { useIsMobile } from '~/common/components/useMatchMedia'; import { useUIPreferencesStore } from '~/common/state/store-ui'; import { AppBar } from './AppBar'; @@ -14,6 +15,31 @@ import { NextRouterProgress } from './NextLoadProgress'; import { useOptimaLayout } from './useOptimaLayout'; +/*function ResponsiveNavigation() { + return <> + { + }} + sx={{ + '& .MuiDrawer-paper': { + width: 256, + boxSizing: 'border-box', + }, + }} + > + + + + + + + ; +}*/ + + /** * Core layout of big-AGI, used by all the Primary applications therein. * @@ -28,22 +54,34 @@ import { useOptimaLayout } from './useOptimaLayout'; export function OptimaLayout(props: { suspendAutoModelsSetup?: boolean, children: React.ReactNode, }) { // external state + const isMobile = useIsMobile(); + + let centerMode = useUIPreferencesStore(state => (isPwa() || isMobile) ? 'full' : state.centerMode); + const { closePreferences, closeShortcuts, openShortcuts, showPreferencesTab, showShortcuts, } = useOptimaLayout(); - const centerMode = useUIPreferencesStore(state => isPwa() ? 'full' : state.centerMode); - - return <> + {/**/} + + {/*a*/} + + {/**/} + {/* Children must make the assumption they're in a flex-col layout */} {props.children} + {/*bb*/} + + {/**/} + {/* Overlay Settings */}