feat(core): show ai-island and navigate to chat page if not available in sidebar (#13085)

close AI-318, AI-317

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit

* **New Features**
* Updated the AI chat button label to "AFFiNE Intelligence" and changed
its icon for improved clarity.
* Enhanced the AI chat button's placement in the sidebar for better
accessibility.
* Improved the AI chat button’s visibility and interaction logic based
on current view and sidebar state.
* **Style**
* Adjusted button styles to disable interaction when hidden, enhancing
user experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
Cats Juice
2025-07-08 21:17:28 +08:00
committed by GitHub
parent 0bd1f10498
commit e04d407b2f
3 changed files with 36 additions and 12 deletions
@@ -18,7 +18,7 @@ import { useI18n } from '@affine/i18n';
import { track } from '@affine/track'; import { track } from '@affine/track';
import type { Store } from '@blocksuite/affine/store'; import type { Store } from '@blocksuite/affine/store';
import { import {
AiIcon, AiOutlineIcon,
AllDocsIcon, AllDocsIcon,
ImportIcon, ImportIcon,
JournalIcon, JournalIcon,
@@ -97,8 +97,8 @@ const AIChatButton = () => {
); );
return ( return (
<MenuLinkItem icon={<AiIcon />} active={aiChatActive} to={'/chat'}> <MenuLinkItem icon={<AiOutlineIcon />} active={aiChatActive} to={'/chat'}>
<span data-testid="ai-chat">Intelligent</span> <span data-testid="ai-chat">AFFiNE Intelligence</span>
</MenuLinkItem> </MenuLinkItem>
); );
}; };
@@ -198,10 +198,10 @@ export const RootAppSidebar = memo((): ReactElement => {
/> />
<AddPageButton /> <AddPageButton />
</div> </div>
<AIChatButton />
<AllDocsButton /> <AllDocsButton />
<AppSidebarJournalButton /> <AppSidebarJournalButton />
{sessionStatus === 'authenticated' && <NotificationButton />} {sessionStatus === 'authenticated' && <NotificationButton />}
<AIChatButton />
<MenuItem <MenuItem
data-testid="slider-bar-workspace-setting-button" data-testid="slider-bar-workspace-setting-button"
icon={<SettingsIcon />} icon={<SettingsIcon />}
@@ -1,12 +1,17 @@
import { WorkbenchService } from '@affine/core/modules/workbench'; import { WorkbenchService } from '@affine/core/modules/workbench';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
import { IslandContainer } from './container'; import { IslandContainer } from './container';
import { AIIcon } from './icons'; import { AIIcon } from './icons';
import { aiIslandBtn, aiIslandWrapper, toolStyle } from './styles.css'; import { aiIslandBtn, aiIslandWrapper, toolStyle } from './styles.css';
const hideChat: Array<string | ((path: string) => boolean)> = [
'/chat',
path => path.includes('attachments'),
];
export const AIIsland = () => { export const AIIsland = () => {
// to make sure ai island is hidden first and animate in // to make sure ai island is hidden first and animate in
const [hide, setHide] = useState(true); const [hide, setHide] = useState(true);
@@ -16,12 +21,33 @@ export const AIIsland = () => {
const haveChatTab = useLiveData( const haveChatTab = useLiveData(
activeView.sidebarTabs$.map(tabs => tabs.some(t => t.id === 'chat')) activeView.sidebarTabs$.map(tabs => tabs.some(t => t.id === 'chat'))
); );
const activeLocation = useLiveData(activeView.location$);
const activeTab = useLiveData(activeView.activeSidebarTab$); const activeTab = useLiveData(activeView.activeSidebarTab$);
const sidebarOpen = useLiveData(workbench.sidebarOpen$); const sidebarOpen = useLiveData(workbench.sidebarOpen$);
useEffect(() => { useEffect(() => {
setHide((sidebarOpen && activeTab?.id === 'chat') || !haveChatTab); let hide = true;
}, [activeTab, haveChatTab, sidebarOpen]); if (haveChatTab) {
hide = !!sidebarOpen && activeTab?.id === 'chat';
} else {
const path = activeLocation.pathname;
hide = hideChat.some(item =>
typeof item === 'string' ? path === item : item(path)
);
}
setHide(hide);
}, [activeLocation.pathname, activeTab, haveChatTab, sidebarOpen]);
const onOpenChat = useCallback(() => {
if (hide) return;
if (haveChatTab) {
workbench.openSidebar();
activeView.activeSidebarTab('chat');
} else {
workbench.open('/chat');
workbench.closeSidebar();
}
}, [activeView, haveChatTab, hide, workbench]);
return ( return (
<IslandContainer className={clsx(toolStyle, { hide })}> <IslandContainer className={clsx(toolStyle, { hide })}>
@@ -29,11 +55,7 @@ export const AIIsland = () => {
<button <button
className={aiIslandBtn} className={aiIslandBtn}
data-testid="ai-island" data-testid="ai-island"
onClick={() => { onClick={onOpenChat}
if (hide) return;
workbench.openSidebar();
activeView.activeSidebarTab('chat');
}}
> >
<AIIcon /> <AIIcon />
</button> </button>
@@ -57,6 +57,8 @@ export const rightSidebarButton = style({
opacity: 0, opacity: 0,
maxWidth: 0, maxWidth: 0,
marginLeft: 0, marginLeft: 0,
// prevent click event from being triggered
pointerEvents: 'none',
}, },
}, },
}); });