Skip to content

Commit

Permalink
feat: support ai layout renderer (#3490)
Browse files Browse the repository at this point in the history
  • Loading branch information
Ricbet authored Apr 3, 2024
1 parent 7a11354 commit de9ce0f
Show file tree
Hide file tree
Showing 14 changed files with 160 additions and 71 deletions.
18 changes: 6 additions & 12 deletions packages/ai-native/src/browser/ai-core.contribution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ import { IEditor } from '@opensumi/ide-editor';
import { BrowserEditorContribution, IEditorFeatureRegistry } from '@opensumi/ide-editor/lib/browser';
import { ISettingRegistry, SettingContribution } from '@opensumi/ide-preferences';

import { AI_CHAT_CONTAINER_VIEW_ID } from '../common';
import { AI_CHAT_CONTAINER_ID, AI_CHAT_VIEW_ID } from '../common';

import { AIEditorContribution } from './ai-editor.contribution';
import { AINativeService } from './ai-native.service';
Expand Down Expand Up @@ -221,24 +221,18 @@ export class AINativeBrowserContribution
}

registerRenderer(registry: SlotRendererRegistry): void {
registry.registerSlotRenderer(AI_CHAT_CONTAINER_VIEW_ID, AIChatTabRenderer);
registry.registerSlotRenderer(AI_CHAT_VIEW_ID, AIChatTabRenderer);
if (this.aiNativeConfigService.layout.useMergeRightWithLeftPanel) {
registry.registerSlotRenderer(SlotLocation.left, AILeftTabRenderer);
registry.registerSlotRenderer(SlotLocation.right, AIRightTabRenderer);
}
}

registerComponent(registry: ComponentRegistry): void {
registry.register(
AI_CHAT_CONTAINER_VIEW_ID,
{
component: AIChatView,
id: AI_CHAT_CONTAINER_VIEW_ID,
},
{
containerId: AI_CHAT_CONTAINER_VIEW_ID,
},
);
registry.register(AI_CHAT_CONTAINER_ID, [], {
component: AIChatView,
containerId: AI_CHAT_CONTAINER_ID,
});
}

registerKeybindings(keybindings: KeybindingRegistry): void {
Expand Down
25 changes: 22 additions & 3 deletions packages/ai-native/src/browser/chat/chat.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,12 @@ import { Icon, Popover, Tooltip } from '@opensumi/ide-core-browser/lib/component
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
import { AISerivceType, ChatFeatureRegistryToken, IAIReporter, localize, uuid } from '@opensumi/ide-core-common';
import { MonacoCommandRegistry } from '@opensumi/ide-editor/lib/browser/monaco-contrib/command/command.service';
import { IMainLayoutService } from '@opensumi/ide-main-layout';
import { isMarkdownString } from '@opensumi/monaco-editor-core/esm/vs/base/common/htmlContent';

import 'react-chat-elements/dist/main.css';
import {
AI_CHAT_VIEW_ID,
IAIChatService,
IChatAgentService,
IChatMessageStructure,
Expand Down Expand Up @@ -174,6 +176,7 @@ export const AIChatView = observer(() => {
const chatAgentService = useInjectable<IChatAgentService>(IChatAgentService);
const chatFeatureRegistry = useInjectable<ChatFeatureRegistry>(ChatFeatureRegistryToken);
const monacoCommandRegistry = useInjectable<MonacoCommandRegistry>(MonacoCommandRegistry);
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);

const containerRef = React.useRef<HTMLDivElement>(null);
const chatInputRef = React.useRef<{ setInputValue: (v: string) => void } | null>(null);
Expand Down Expand Up @@ -448,18 +451,34 @@ export const AIChatView = observer(() => {
setTheme(value);
};

const handleCloseChatView = React.useCallback(() => {
layoutService.toggleSlot(AI_CHAT_VIEW_ID);
}, [layoutService]);

return (
<div className={styles.ai_chat_view}>
<div className={styles.header_container}>
<div className={styles.left}>
<span className={styles.title}>{aiAssistantName}</span>
</div>
<div className={styles.right}>
<Popover insertClass={styles.popover_icon} id={'ai-chat-header-clear'} title='清空'>
<Popover
insertClass={styles.popover_icon}
id={'ai-chat-header-clear'}
title={localize('aiNative.operate.clear.title')}
>
<EnhanceIcon wrapperClassName={styles.action_btn} className={getIcon('clear')} onClick={handleClear} />
</Popover>
<Popover insertClass={styles.popover_icon} id={'ai-chat-header-close'} title='关闭'>
<EnhanceIcon wrapperClassName={styles.action_btn} className={getIcon('window-close')} />
<Popover
insertClass={styles.popover_icon}
id={'ai-chat-header-close'}
title={localize('aiNative.operate.close.title')}
>
<EnhanceIcon
wrapperClassName={styles.action_btn}
className={getIcon('window-close')}
onClick={handleCloseChatView}
/>
</Popover>
</div>
</div>
Expand Down
42 changes: 42 additions & 0 deletions packages/ai-native/src/browser/layout/ai-layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from 'react';

import { SlotRenderer } from '@opensumi/ide-core-browser';
import { BoxPanel, SplitPanel, getStorageValue } from '@opensumi/ide-core-browser/lib/components';

import { AI_CHAT_VIEW_ID } from '../../common';

export const AILayout = () => {
const { layout } = getStorageValue();

return (
<BoxPanel direction='top-to-bottom'>
<SlotRenderer id='top' defaultSize={35} slot='top' z-index={2} />
<SplitPanel id='main-horizontal-ai' flex={1} direction={'left-to-right'} useDomSize={true}>
<SplitPanel id='main-horizontal' flex={1} flexGrow={1} direction={'left-to-right'} useDomSize={true}>
<SlotRenderer
slot='left'
isTabbar={true}
defaultSize={layout.left?.currentId ? layout.left?.size || 310 : 49}
minResize={280}
maxResize={480}
minSize={49}
/>
<SplitPanel id='main-vertical' minResize={300} flexGrow={1} direction='top-to-bottom'>
<SlotRenderer flex={2} flexGrow={1} minResize={200} slot='main' />
<SlotRenderer flex={1} defaultSize={layout.bottom?.size} minResize={160} slot='bottom' isTabbar={true} />
</SplitPanel>
<SlotRenderer slot='right' isTabbar={true} defaultSize={360} maxResize={360} minResize={280} minSize={0} />
</SplitPanel>
<SlotRenderer
slot={AI_CHAT_VIEW_ID}
isTabbar={true}
defaultSize={layout.AI_Chat?.currentId ? layout.AI_Chat?.size || 360 : 0}
maxResize={420}
minResize={280}
minSize={0}
/>
</SplitPanel>
<SlotRenderer id='statusbar' defaultSize={24} slot='statusBar' />
</BoxPanel>
);
};
6 changes: 3 additions & 3 deletions packages/ai-native/src/browser/layout/layout-config.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { SlotLocation } from '@opensumi/ide-core-browser';

import { AI_CHAT_CONTAINER_VIEW_ID, AI_MENUBAR_CONTAINER_VIEW_ID } from '../../common';
import { AI_CHAT_CONTAINER_ID, AI_CHAT_VIEW_ID, AI_MENUBAR_CONTAINER_VIEW_ID } from '../../common';

export const AIChatLayoutConfig = {
[AI_CHAT_CONTAINER_VIEW_ID]: {
modules: [AI_CHAT_CONTAINER_VIEW_ID],
[AI_CHAT_VIEW_ID]: {
modules: [AI_CHAT_CONTAINER_ID],
},
};

Expand Down
16 changes: 14 additions & 2 deletions packages/ai-native/src/browser/layout/menu-bar/menu-bar.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ import * as React from 'react';

import { AINativeConfigService, SlotLocation, SlotRenderer, getIcon, useInjectable } from '@opensumi/ide-core-browser';
import { Icon } from '@opensumi/ide-core-browser/lib/components';
import { EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
import { AILogoAvatar, EnhanceIcon } from '@opensumi/ide-core-browser/lib/components/ai-native';
import { VIEW_CONTAINERS } from '@opensumi/ide-core-browser/lib/layout/view-id';
import { AbstractContextMenuService, ICtxMenuRenderer, MenuId } from '@opensumi/ide-core-browser/lib/menu/next';
import { CommandService } from '@opensumi/ide-core-common';
import { IMainLayoutService } from '@opensumi/ide-main-layout';

import { AI_CHAT_VIEW_ID } from '../../../common/';
import { AI_MENU_BAR_LEFT, AI_MENU_BAR_RIGHT } from '../layout-config';

import opensumiLogo from './logo.svg';
Expand Down Expand Up @@ -56,7 +57,10 @@ const AIMenuBarRender = () => {
});
}, [anchor, extraTopMenus]);

const logo = React.useMemo(() => aiNativeConfigService.layout.menubarLogo || opensumiLogo, [aiNativeConfigService.layout.menubarLogo]);
const logo = React.useMemo(
() => aiNativeConfigService.layout.menubarLogo || opensumiLogo,
[aiNativeConfigService.layout.menubarLogo],
);

return (
<>
Expand All @@ -74,6 +78,7 @@ export const AIMenuBarView = () => {
const commandService = useInjectable<CommandService>(CommandService);
const mainLayoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
const aiNativeConfigService = useInjectable<AINativeConfigService>(AINativeConfigService);
const layoutService = useInjectable<IMainLayoutService>(IMainLayoutService);
const [isVisiablePanel, setIsVisiablePanel] = React.useState<boolean>(false);

React.useEffect(() => {
Expand All @@ -94,6 +99,10 @@ export const AIMenuBarView = () => {
return !!tabbarService.currentContainerId;
}, [mainLayoutService]);

const handleChatVisible = React.useCallback(() => {
layoutService.toggleSlot(AI_CHAT_VIEW_ID);
}, [layoutService]);

return (
<div
id={VIEW_CONTAINERS.MENUBAR}
Expand All @@ -115,6 +124,9 @@ export const AIMenuBarView = () => {
</div>
<div className={styles.right}>
<SlotRenderer slot={AI_MENU_BAR_RIGHT} flex={1} overflow={'initial'} />
<div className={styles.ai_switch} onClick={handleChatVisible}>
<AILogoAvatar iconClassName={styles.avatar_icon_large} />
</div>
</div>
</div>
</div>
Expand Down
41 changes: 30 additions & 11 deletions packages/ai-native/src/browser/layout/tabbar.view.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import cls from 'classnames';
import React, { useCallback } from 'react';
import React, { useCallback, useEffect } from 'react';

import { ComponentRegistryInfo, SlotLocation, useContextMenus, useInjectable } from '@opensumi/ide-core-browser';
import { EDirection } from '@opensumi/ide-core-browser/lib/components';
Expand All @@ -13,15 +13,34 @@ import { IMenu } from '@opensumi/ide-core-browser/lib/menu/next';
import { localize } from '@opensumi/ide-core-common';
import { DesignLeftTabRenderer, DesignRightTabRenderer } from '@opensumi/ide-design/lib/browser/layout/tabbar.view';
import { IMainLayoutService } from '@opensumi/ide-main-layout';
import { LeftTabbarRenderer } from '@opensumi/ide-main-layout/lib/browser/tabbar/bar.view';
import {
IconElipses,
IconTabView,
LeftTabbarRenderer,
RightTabbarRenderer,
TabbarViewBase,
} from '@opensumi/ide-main-layout/lib/browser/tabbar/bar.view';
import { BaseTabPanelView, ContainerView } from '@opensumi/ide-main-layout/lib/browser/tabbar/panel.view';
import { TabRendererBase } from '@opensumi/ide-main-layout/lib/browser/tabbar/renderer.view';
import { TabRendererBase, TabbarConfig } from '@opensumi/ide-main-layout/lib/browser/tabbar/renderer.view';
import { TabbarService, TabbarServiceFactory } from '@opensumi/ide-main-layout/lib/browser/tabbar/tabbar.service';

import { AI_CHAT_CONTAINER_VIEW_ID } from '../../common';
import { AI_CHAT_VIEW_ID } from '../../common';

import styles from './layout.module.less';

const ChatTabbarRenderer: React.FC = () => {
const { side } = React.useContext(TabbarConfig);
const tabbarService: TabbarService = useInjectable(TabbarServiceFactory)(side);
useEffect(() => {
tabbarService.setIsLatter(true);
}, [tabbarService]);
return (
<div style={{ width: 0 }}>
<TabbarViewBase tabSize={0} MoreTabView={IconElipses} TabView={IconTabView} barSize={0} panelBorderSize={1} />
</div>
);
};

export const AIChatTabRenderer = ({
className,
components,
Expand All @@ -30,13 +49,13 @@ export const AIChatTabRenderer = ({
components: ComponentRegistryInfo[];
}) => (
<TabRendererBase
side={AI_CHAT_CONTAINER_VIEW_ID}
direction={EDirection.RightToLeft}
side={AI_CHAT_VIEW_ID}
direction={EDirection.LeftToRight}
id={styles.ai_chat_panel}
className={cls(className, `${AI_CHAT_CONTAINER_VIEW_ID}-slot`)}
className={cls(className, `${AI_CHAT_VIEW_ID}-slot`)}
components={components}
TabbarView={() => null}
TabpanelView={() => <BaseTabPanelView PanelView={ContainerView} currentContainerId={AI_CHAT_CONTAINER_VIEW_ID} />}
TabbarView={() => <ChatTabbarRenderer />}
TabpanelView={() => <BaseTabPanelView PanelView={ContainerView} />}
/>
);

Expand Down Expand Up @@ -98,7 +117,7 @@ export const AIRightTabRenderer = ({
const tabbarService: TabbarService = useInjectable(TabbarServiceFactory)(SlotLocation.right);

const handleClose = useCallback(() => {
tabbarService.currentContainerId = '';
tabbarService.updateCurrentContainerId('');
}, []);

const ContainerViewFn = useCallback((props: { component: ComponentRegistryInfo; side: string; titleMenu: IMenu }) => {
Expand Down Expand Up @@ -127,7 +146,7 @@ export const AIRightTabRenderer = ({
return (
<DesignRightTabRenderer
components={components}
tabbarView={() => null}
tabbarView={() => <RightTabbarRenderer barSize={0} style={{ width: 0 }} />}
tabpanelView={() => <BaseTabPanelView PanelView={ContainerViewFn} />}
/>
);
Expand Down
3 changes: 2 additions & 1 deletion packages/ai-native/src/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@ import {
export const IAINativeService = Symbol('IAINativeService');

export const AIInlineChatContentWidget = 'AI_Inline_Chat_Content_Widget';
export const AI_CHAT_CONTAINER_VIEW_ID = 'AI_Chat';
export const AI_CHAT_VIEW_ID = 'AI_Chat';
export const AI_CHAT_CONTAINER_ID = 'AI_Chat_Container';
export const AI_MENUBAR_CONTAINER_VIEW_ID = 'AI_menubar';

export const AI_SLASH = '/';
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/common/en-US.lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1464,6 +1464,8 @@ export const localizationBundle = {
'aiNative.operate.discard.title': 'Discard',
'aiNative.operate.afresh.title': 'Afresh',
'aiNative.operate.stop.title': 'Stop',
'aiNative.operate.close.title': 'Close',
'aiNative.operate.clear.title': 'Clear',

'aiNative.chat.welcome.loading.text': 'Initializing...',
// #endregion AI Native
Expand Down
2 changes: 2 additions & 0 deletions packages/i18n/src/common/zh-CN.lang.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1231,6 +1231,8 @@ export const localizationBundle = {
'aiNative.operate.discard.title': '丢弃',
'aiNative.operate.afresh.title': '重新生成',
'aiNative.operate.stop.title': '停止',
'aiNative.operate.close.title': '关闭',
'aiNative.operate.clear.title': '清空',

'aiNative.chat.welcome.loading.text': '初始化中...',
// #endregion AI Native
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -303,7 +303,7 @@ describe('main layout test', () => {
handler.setCollapsed('test-view-id5', true);
});
expect(handler.isCollapsed('test-view-id5')).toBeTruthy();
expect(mockCb).toBeCalledTimes(2);
expect(mockCb).toBeCalledTimes(4);
let newTitle = 'new title';
act(() => {
handler.setBadge('20');
Expand Down
6 changes: 4 additions & 2 deletions packages/main-layout/src/browser/tabbar/bar.view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,8 @@ export const TextElipses: React.FC = () => (
</div>
);

export const RightTabbarRenderer: React.FC = () => {
export const RightTabbarRenderer: React.FC<{ barSize?: number; style?: React.CSSProperties }> = (props) => {
const { barSize = 48, style } = props;
const { side } = React.useContext(TabbarConfig);
const tabbarService: TabbarService = useInjectable(TabbarServiceFactory)(side);

Expand All @@ -290,14 +291,15 @@ export const RightTabbarRenderer: React.FC = () => {
<div
id={VIEW_CONTAINERS.RIGHT_TABBAR}
className={styles_right_tab_bar}
style={style}
onContextMenu={tabbarService.handleContextMenu}
>
<TabbarViewBase
tabSize={48}
MoreTabView={IconElipses}
tabClassName={styles_right_tab}
TabView={IconTabView}
barSize={48}
barSize={barSize}
panelBorderSize={1}
/>
</div>
Expand Down
Loading

1 comment on commit de9ce0f

@opensumi
Copy link
Contributor

@opensumi opensumi bot commented on de9ce0f Apr 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Release Candidate Summary:

Released 🚀 2.27.3-rc-1712113547.0

2.27.3-rc-1712113547.0

user input ref: main

de9ce0f feat: support ai layout renderer (#3490)

Please sign in to comment.