feat: update main chat and system chat UI
This commit is contained in:
@@ -1,12 +1,23 @@
|
||||
import { Layout } from 'antd';
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
import { Outlet, useLocation, useNavigate, useSearchParams } from 'react-router-dom';
|
||||
import { useGestureLayer, useGesturePageState, useSearchLayer } from '../../../layer';
|
||||
import { useAppStore } from '../../../store';
|
||||
import { useTokenAccess } from '../tokenAccess';
|
||||
import { syncAppConfigFromServer, useAppConfig } from '../appConfig';
|
||||
import { getChatActionContextSnapshot } from '../chatActionContextStore';
|
||||
import { ChatRuntimeBridgeV2 } from '../ChatRuntimeBridgeV2';
|
||||
import { SystemChatPanel } from '../SystemChatPanel';
|
||||
import { ScopedChatRoomsWindow, ScopedChatRoomsWindowDock } from '../ScopedChatRoomsWindow';
|
||||
import {
|
||||
removeMinimizedIsolatedChatRoomEntryByScope,
|
||||
useActiveIsolatedChatRoomScope,
|
||||
useIsolatedChatRoomsWindowOpen,
|
||||
writeActiveIsolatedChatRoomScope,
|
||||
writeIsolatedChatRoomsWindowOpen,
|
||||
} from '../isolatedChatRoomScopeStore';
|
||||
import { useUnreadCounts } from '../chatV2/hooks/useUnreadCounts';
|
||||
import { normalizeIsolatedChatRoomScope } from '../isolatedChatRooms';
|
||||
import { matchesShortcut, isTypingTarget, scrollToElement } from '../mainView/utils';
|
||||
import { MainContent } from '../MainContent';
|
||||
import { MainHeader } from '../MainHeader';
|
||||
@@ -43,6 +54,8 @@ import {
|
||||
type TopMenuKey,
|
||||
} from '../routes';
|
||||
|
||||
const E_READER_IMMERSIVE_BODY_CLASS = 'play-app-e-reader-immersive';
|
||||
|
||||
function parseRoute(pathname: string): {
|
||||
topMenu: TopMenuKey;
|
||||
docsMenu: string;
|
||||
@@ -90,6 +103,8 @@ function parseRoute(pathname: string): {
|
||||
first === 'history' ||
|
||||
first === 'automation-type' ||
|
||||
first === 'automation-context' ||
|
||||
first === 'token-setting' ||
|
||||
first === 'shared-resource' ||
|
||||
first === 'server-command')
|
||||
) {
|
||||
return {
|
||||
@@ -105,11 +120,13 @@ function parseRoute(pathname: string): {
|
||||
if (
|
||||
top === 'chat' &&
|
||||
(first === 'live' ||
|
||||
first === 'rooms' ||
|
||||
first === 'changes' ||
|
||||
first === 'resources' ||
|
||||
first === 'errors' ||
|
||||
first === 'manage' ||
|
||||
first === 'manage-defaults')
|
||||
first === 'manage-defaults' ||
|
||||
first === 'manage-share')
|
||||
) {
|
||||
return {
|
||||
topMenu: 'chat',
|
||||
@@ -121,7 +138,7 @@ function parseRoute(pathname: string): {
|
||||
};
|
||||
}
|
||||
|
||||
if (top === 'play' && (first === 'layout' || first === 'test' || first === 'cbt')) {
|
||||
if (top === 'play' && (first === 'layout' || first === 'draw' || first === 'apps' || first === 'test' || first === 'cbt')) {
|
||||
return {
|
||||
topMenu: 'play',
|
||||
docsMenu: DOCS_DEFAULT_FOLDER,
|
||||
@@ -212,14 +229,22 @@ function resolveSidebarOpenKeys(
|
||||
}
|
||||
|
||||
if (topMenu === 'plans') {
|
||||
return planMenu === 'server-command' ? ['server-group'] : ['plan-group'];
|
||||
if (planMenu === 'server-command') {
|
||||
return ['server-group'];
|
||||
}
|
||||
|
||||
if (planMenu === 'token-setting' || planMenu === 'shared-resource') {
|
||||
return ['token-management-group'];
|
||||
}
|
||||
|
||||
return ['plan-group'];
|
||||
}
|
||||
|
||||
if (chatMenu === 'errors') {
|
||||
return ['app-log-group'];
|
||||
}
|
||||
|
||||
return chatMenu === 'manage' || chatMenu === 'manage-defaults' ? ['chat-manage-group'] : ['codex-live-group'];
|
||||
return chatMenu === 'manage' || chatMenu === 'manage-defaults' ? ['chat-manage-group'] : ['chat-group'];
|
||||
}
|
||||
|
||||
export function MainLayout() {
|
||||
@@ -227,13 +252,18 @@ export function MainLayout() {
|
||||
const location = useLocation();
|
||||
const navigate = useNavigate();
|
||||
const [searchParams] = useSearchParams();
|
||||
const { setCurrentPage, setFocusedComponentId } = useAppStore();
|
||||
const { currentPage, focusedComponentId, setCurrentPage, setFocusedComponentId } = useAppStore();
|
||||
const { hasAccess } = useTokenAccess();
|
||||
const activeScopedChatRoomScope = useActiveIsolatedChatRoomScope();
|
||||
const isScopedChatRoomsWindowOpen = useIsolatedChatRoomsWindowOpen();
|
||||
const appConfig = useAppConfig();
|
||||
const { openSearch, setOptions: setSearchOptions } = useSearchLayer();
|
||||
const layoutData = useMainLayoutData();
|
||||
const routeState = useMemo(() => parseRoute(location.pathname), [location.pathname]);
|
||||
const [isMobileViewport, setIsMobileViewport] = useState(() => getIsMobileViewport());
|
||||
const [isEReaderImmersiveActive, setIsEReaderImmersiveActive] = useState(() =>
|
||||
typeof document !== 'undefined' ? document.body.classList.contains(E_READER_IMMERSIVE_BODY_CLASS) : false,
|
||||
);
|
||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(() =>
|
||||
resolveSidebarCollapsedForViewport(getIsSidebarOverlayViewport(routeState.topMenu)),
|
||||
);
|
||||
@@ -250,14 +280,58 @@ export function MainLayout() {
|
||||
const [planQuickFilterRequestKey, setPlanQuickFilterRequestKey] = useState(routeState.planMenu === 'release' ? 1 : 0);
|
||||
const { componentSampleEntries, widgetSampleEntries, componentSamples, widgetSamples, docsDocuments, savedLayouts, savedLayoutsReady, setSavedLayouts, docFolders } = layoutData;
|
||||
const { chatUnreadCount } = useUnreadCounts();
|
||||
const navigateWithinApp = (path: string, options?: { replace?: boolean }) => {
|
||||
const nextPath = previewRuntime ? appendPreviewRuntimeSearch(path, location.search) : path;
|
||||
navigate(nextPath, options);
|
||||
const navigateWithinApp = (path: string, options?: { replace?: boolean; resetSearch?: boolean }) => {
|
||||
const baseSearch = options?.resetSearch ? '' : location.search;
|
||||
const nextPath = previewRuntime ? appendPreviewRuntimeSearch(path, baseSearch) : path;
|
||||
navigate(nextPath, options?.replace == null ? undefined : { replace: options.replace });
|
||||
};
|
||||
|
||||
const openScopedChatRooms = useCallback(() => {
|
||||
const actionSnapshot = getChatActionContextSnapshot();
|
||||
const scope = normalizeIsolatedChatRoomScope({
|
||||
topMenu: currentPage.topMenu,
|
||||
menuTitle: currentPage.title,
|
||||
featureTitle: actionSnapshot.featureTitle ?? focusedComponentId ?? currentPage.title,
|
||||
focusedComponentId,
|
||||
pageUrl: typeof window !== 'undefined' ? window.location.href : '',
|
||||
selectionSummary: actionSnapshot.selectionSummary,
|
||||
selectionIds: actionSnapshot.selectionIds,
|
||||
sourceAppId: actionSnapshot.sourceAppId,
|
||||
launchedAt: new Date().toISOString(),
|
||||
});
|
||||
|
||||
writeActiveIsolatedChatRoomScope(scope);
|
||||
removeMinimizedIsolatedChatRoomEntryByScope(scope);
|
||||
|
||||
if (routeState.chatMenu === 'rooms') {
|
||||
writeIsolatedChatRoomsWindowOpen(false);
|
||||
return;
|
||||
}
|
||||
|
||||
writeIsolatedChatRoomsWindowOpen(true);
|
||||
}, [currentPage.title, currentPage.topMenu, focusedComponentId, routeState.chatMenu]);
|
||||
|
||||
useEffect(() => {
|
||||
void syncAppConfigFromServer();
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (typeof document === 'undefined') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const body = document.body;
|
||||
const syncEReaderImmersiveState = () => {
|
||||
setIsEReaderImmersiveActive(body.classList.contains(E_READER_IMMERSIVE_BODY_CLASS));
|
||||
};
|
||||
|
||||
syncEReaderImmersiveState();
|
||||
const observer = new MutationObserver(syncEReaderImmersiveState);
|
||||
observer.observe(body, { attributes: true, attributeFilter: ['class'] });
|
||||
|
||||
return () => {
|
||||
observer.disconnect();
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const mediaQuery = window.matchMedia('(max-width: 768px)');
|
||||
@@ -331,45 +405,50 @@ export function MainLayout() {
|
||||
setActivePlanQuickFilter((current) => (current === 'working' || current === 'automation-failed' ? current : null));
|
||||
}, [routeState.planMenu, routeState.topMenu]);
|
||||
|
||||
const gestureLayer = useMemo(
|
||||
() => ({
|
||||
id: 'main-layout',
|
||||
enabled:
|
||||
!isEReaderImmersiveActive &&
|
||||
!(isMobileViewport && routeState.topMenu === 'docs' && routeState.docsMenu === DOCS_DEFAULT_FOLDER),
|
||||
gestures: [
|
||||
{
|
||||
id: 'mobile-top-right-pull-alert',
|
||||
activeStates: ['anyway'],
|
||||
mobileOnly: true,
|
||||
trigger: 'pull-down-top-right' as const,
|
||||
onTrigger: () => {
|
||||
openSearch();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mobile-middle-right-search-window',
|
||||
activeStates: ['anyway'],
|
||||
mobileOnly: true,
|
||||
trigger: 'pull-left-middle-right' as const,
|
||||
hotZoneSize: 36,
|
||||
minDistance: 180,
|
||||
minViewportDistanceRatio: 0.35,
|
||||
maxHorizontalDrift: 72,
|
||||
onTrigger: openScopedChatRooms,
|
||||
},
|
||||
],
|
||||
}),
|
||||
[isEReaderImmersiveActive, isMobileViewport, openScopedChatRooms, openSearch, routeState.docsMenu, routeState.topMenu],
|
||||
);
|
||||
|
||||
useGesturePageState('anyway');
|
||||
useGestureLayer({
|
||||
id: 'main-layout',
|
||||
enabled: !(isMobileViewport && routeState.topMenu === 'docs' && routeState.docsMenu === DOCS_DEFAULT_FOLDER),
|
||||
gestures: [
|
||||
{
|
||||
id: 'mobile-top-right-pull-alert',
|
||||
activeStates: ['anyway'],
|
||||
mobileOnly: true,
|
||||
trigger: 'pull-down-top-right',
|
||||
onTrigger: () => {
|
||||
openSearch();
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'mobile-middle-right-search-window',
|
||||
activeStates: ['anyway'],
|
||||
mobileOnly: true,
|
||||
trigger: 'pull-left-middle-right',
|
||||
hotZoneSize: 36,
|
||||
minDistance: 180,
|
||||
minViewportDistanceRatio: 0.35,
|
||||
maxHorizontalDrift: 72,
|
||||
onTrigger: () => {
|
||||
openSearch('window');
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
useGestureLayer(gestureLayer);
|
||||
|
||||
useEffect(() => {
|
||||
const handleWindowKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.repeat || isTypingTarget(event.target)) {
|
||||
if (event.repeat || isTypingTarget(event.target) || isEReaderImmersiveActive) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (matchesShortcut(event, appConfig.gestureShortcuts.openWindowSearch)) {
|
||||
event.preventDefault();
|
||||
openSearch('window');
|
||||
openScopedChatRooms();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -384,7 +463,13 @@ export function MainLayout() {
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleWindowKeyDown);
|
||||
};
|
||||
}, [appConfig.gestureShortcuts.openSearch, appConfig.gestureShortcuts.openWindowSearch, openSearch]);
|
||||
}, [
|
||||
appConfig.gestureShortcuts.openSearch,
|
||||
appConfig.gestureShortcuts.openWindowSearch,
|
||||
isEReaderImmersiveActive,
|
||||
openScopedChatRooms,
|
||||
openSearch,
|
||||
]);
|
||||
|
||||
const selectedDocs = useMemo(
|
||||
() => docsDocuments.filter((document) => document.folder === routeState.docsMenu),
|
||||
@@ -448,6 +533,10 @@ export function MainLayout() {
|
||||
const planMenuItems = useMemo(() => buildPlanMenuItems(hasAccess), [hasAccess]);
|
||||
const chatMenuItems = useMemo(() => buildChatMenuItems(hasAccess, chatUnreadCount), [chatUnreadCount, hasAccess]);
|
||||
const playMenuItems = useMemo(() => buildPlayMenuItems(savedLayouts), [savedLayouts]);
|
||||
const activePlayAppId = routeState.topMenu === 'play' && routeState.playMenu === 'apps'
|
||||
? searchParams.get('app')?.trim() ?? ''
|
||||
: '';
|
||||
const isPlayAppFullscreen = activePlayAppId.length > 0;
|
||||
const initialSelectedPlanId = Number(searchParams.get('planId'));
|
||||
const initialSelectedWorkId = searchParams.get('workId');
|
||||
|
||||
@@ -477,8 +566,8 @@ export function MainLayout() {
|
||||
}}
|
||||
>
|
||||
<Layout className={`app-shell app-shell--docs-api${previewRuntime ? ' app-shell--preview-runtime' : ''}`}>
|
||||
{routeState.topMenu === 'chat' ? null : <ChatRuntimeBridgeV2 />}
|
||||
{contentExpanded ? null : (
|
||||
{routeState.topMenu === 'chat' || isPlayAppFullscreen ? null : <ChatRuntimeBridgeV2 />}
|
||||
{contentExpanded || isPlayAppFullscreen ? null : (
|
||||
<MainHeader
|
||||
activeTopMenu={routeState.topMenu}
|
||||
sidebarCollapsed={sidebarCollapsed}
|
||||
@@ -494,6 +583,9 @@ export function MainLayout() {
|
||||
navigateWithinApp(resolveTopMenuPath(menu, currentDocsFolder));
|
||||
setSidebarCollapsed(resolveSidebarCollapsedForViewport(getIsSidebarOverlayViewport(menu)));
|
||||
}}
|
||||
onOpenSearch={() => {
|
||||
openSearch();
|
||||
}}
|
||||
onOpenPlanQuickFilter={(filter) => {
|
||||
const targetPlanMenu = resolvePlanQuickFilterMenu(filter);
|
||||
setActivePlanQuickFilter(filter);
|
||||
@@ -506,7 +598,7 @@ export function MainLayout() {
|
||||
)}
|
||||
|
||||
<Layout className="app-shell__body">
|
||||
{contentExpanded || (isSidebarOverlayViewport && sidebarCollapsed) ? null : (
|
||||
{contentExpanded || isPlayAppFullscreen || (isSidebarOverlayViewport && sidebarCollapsed) ? null : (
|
||||
<MainSidebar
|
||||
activeTopMenu={routeState.topMenu}
|
||||
hasAccess={hasAccess}
|
||||
@@ -545,14 +637,14 @@ export function MainLayout() {
|
||||
}
|
||||
}}
|
||||
onSelectChatMenu={(key) => {
|
||||
navigateWithinApp(buildChatPath(key));
|
||||
navigateWithinApp(buildChatPath(key), { resetSearch: true });
|
||||
if (isSidebarOverlayViewport) {
|
||||
setSidebarCollapsed(true);
|
||||
}
|
||||
}}
|
||||
onSelectPlayMenu={(key) => {
|
||||
const savedLayoutId = resolveSavedLayoutIdFromMenuKey(key);
|
||||
navigateWithinApp(savedLayoutId ? buildSavedLayoutPath(savedLayoutId) : buildPlayPath(key as 'layout' | 'test'));
|
||||
navigateWithinApp(savedLayoutId ? buildSavedLayoutPath(savedLayoutId) : buildPlayPath(key));
|
||||
if (isSidebarOverlayViewport) {
|
||||
setSidebarCollapsed(true);
|
||||
}
|
||||
@@ -572,6 +664,16 @@ export function MainLayout() {
|
||||
<Outlet />
|
||||
</MainContent>
|
||||
</Layout>
|
||||
{routeState.chatMenu !== 'rooms' && isScopedChatRoomsWindowOpen ? (
|
||||
<ScopedChatRoomsWindow
|
||||
onClose={() => {
|
||||
writeIsolatedChatRoomsWindowOpen(false);
|
||||
}}
|
||||
>
|
||||
<SystemChatPanel lockOuterScrollOnMobile />
|
||||
</ScopedChatRoomsWindow>
|
||||
) : null}
|
||||
{routeState.chatMenu !== 'rooms' ? <ScopedChatRoomsWindowDock /> : null}
|
||||
</Layout>
|
||||
</MainLayoutContextProvider>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user