feat: add ai-is-land (#7259)

This commit is contained in:
regischen 2024-06-20 02:52:14 +00:00
parent 0d711667a8
commit ddf72733e1
8 changed files with 156 additions and 44 deletions

View File

@ -0,0 +1,27 @@
export const AIIcon = () => {
return (
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<g clipPath="url(#clip0_5864_85390)">
<path
d="M11.2812 5.49104C11.2403 5.13024 10.9353 4.85751 10.5722 4.85714C10.2091 4.85677 9.90345 5.12887 9.86185 5.48959C9.59131 7.83515 8.89003 9.48448 7.75868 10.6158C6.62734 11.7472 4.97801 12.4485 2.63244 12.719C2.27173 12.7606 1.99963 13.0662 2 13.4293C2.00037 13.7924 2.2731 14.0975 2.63389 14.1383C4.94069 14.3996 6.62508 15.1006 7.78328 16.2379C8.93713 17.3709 9.65305 19.0198 9.85994 21.3489C9.89271 21.7178 10.2019 22.0004 10.5722 22C10.9425 21.9996 11.2511 21.7162 11.2831 21.3473C11.4813 19.0565 12.1966 17.3729 13.3562 16.2133C14.5157 15.0537 16.1994 14.3385 18.4902 14.1402C18.8591 14.1083 19.1424 13.7997 19.1429 13.4294C19.1433 13.0591 18.8606 12.7499 18.4918 12.7171C16.1627 12.5102 14.5137 11.7943 13.3807 10.6404C12.2435 9.48222 11.5425 7.79783 11.2812 5.49104Z"
fill="#77757D"
/>
<path
d="M18.9427 2.24651C18.9268 2.1062 18.8082 2.00014 18.667 2C18.5257 1.99986 18.4069 2.10567 18.3907 2.24595C18.2855 3.15811 18.0128 3.79952 17.5728 4.23949C17.1329 4.67946 16.4914 4.95218 15.5793 5.05739C15.439 5.07356 15.3332 5.19241 15.3333 5.33362C15.3335 5.47482 15.4395 5.59345 15.5798 5.60935C16.4769 5.71096 17.132 5.98357 17.5824 6.42584C18.0311 6.86644 18.3095 7.50771 18.39 8.41347C18.4027 8.55691 18.523 8.66683 18.667 8.66667C18.811 8.6665 18.931 8.55632 18.9434 8.41284C19.0205 7.52199 19.2987 6.86723 19.7496 6.41629C20.2006 5.96534 20.8553 5.68719 21.7462 5.61008C21.8896 5.59766 21.9998 5.47765 22 5.33365C22.0002 5.18964 21.8902 5.06939 21.7468 5.05664C20.841 4.97619 20.1998 4.69777 19.7592 4.24905C19.3169 3.79864 19.0443 3.1436 18.9427 2.24651Z"
fill="#77757D"
/>
</g>
<defs>
<clipPath id="clip0_5864_85390">
<rect width="24" height="24" fill="white" />
</clipPath>
</defs>
</svg>
);
};

View File

@ -0,0 +1,65 @@
import type { SidebarTabName } from '@affine/core/modules/multi-tab-sidebar';
import { RightSidebarService } from '@affine/core/modules/right-sidebar';
import {
DocsService,
GlobalContextService,
GlobalStateService,
LiveData,
useLiveData,
useService,
} from '@toeverything/infra';
import { useCallback } from 'react';
import { ToolContainer } from '../../workspace';
import { AIIcon } from './icons';
import { StyledIsland, StyledTriggerWrapper } from './style';
export const RIGHT_SIDEBAR_TABS_ACTIVE_KEY =
'app:settings:rightsidebar:tabs:active';
export const AIIsland = () => {
const docId = useLiveData(
useService(GlobalContextService).globalContext.docId.$
);
const docRecordList = useService(DocsService).list;
const doc = useLiveData(docId ? docRecordList.doc$(docId) : undefined);
const mode = useLiveData(doc?.mode$);
const globalState = useService(GlobalStateService).globalState;
const activeTabName = useLiveData(
LiveData.from(
globalState.watch<SidebarTabName>(RIGHT_SIDEBAR_TABS_ACTIVE_KEY),
'journal'
)
);
const setActiveTabName = useCallback(
(name: string) => {
globalState.set(RIGHT_SIDEBAR_TABS_ACTIVE_KEY, name);
},
[globalState]
);
const rightSidebar = useService(RightSidebarService).rightSidebar;
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
return (
<ToolContainer>
<StyledIsland
spread={true}
data-testid="ai-island"
onClick={() => {
if (rightSidebarOpen) return;
rightSidebar.isOpen$;
rightSidebar.open();
if (activeTabName !== 'chat') {
setActiveTabName('chat');
}
}}
inEdgelessPage={!!docId && mode === 'edgeless'}
>
<StyledTriggerWrapper data-testid="faq-icon">
<AIIcon />
</StyledTriggerWrapper>
</StyledIsland>
</ToolContainer>
);
};

View File

@ -0,0 +1,43 @@
import { displayFlex, positionAbsolute, styled } from '@affine/component';
export const StyledIsland = styled('div')<{
spread: boolean;
inEdgelessPage?: boolean;
}>(({ spread, inEdgelessPage }) => {
return {
width: '44px',
position: 'relative',
boxShadow: spread
? 'var(--affine-menu-shadow)'
: inEdgelessPage
? 'var(--affine-menu-shadow)'
: 'unset',
padding: '0 4px 44px',
borderRadius: '50%',
background: spread
? 'var(--affine-background-overlay-panel-color)'
: 'var(--affine-background-primary-color)',
':hover': {
background: spread ? undefined : 'var(--affine-white)',
boxShadow: spread ? undefined : 'var(--affine-menu-shadow)',
},
};
});
export const StyledTriggerWrapper = styled('div')<{
spread?: boolean;
}>(({ spread }) => {
return {
width: '36px',
height: '36px',
cursor: 'pointer',
color: 'var(--affine-icon-color)',
borderRadius: '5px',
fontSize: '24px',
...displayFlex('center', 'center'),
...positionAbsolute({ left: '4px', bottom: '4px' }),
':hover': {
backgroundColor: spread ? 'var(--affine-hover-color)' : undefined,
},
};
});

View File

@ -1,4 +1,3 @@
import { HubIsland } from '@affine/core/components/affine/hub-island';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import type { DragEndEvent } from '@dnd-kit/core';
import {
@ -116,7 +115,6 @@ export const SplitView = ({
data-client-border={appSettings.clientBorder}
{...attrs}
>
<HubIsland />
<DndContext
sensors={sensors}
collisionDetection={closestCenter}

View File

@ -2,6 +2,10 @@ import { Scrollable } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { AIProvider } from '@affine/core/blocksuite/presets/ai';
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
import {
AIIsland,
RIGHT_SIDEBAR_TABS_ACTIVE_KEY,
} from '@affine/core/components/pure/ai-island';
import { useAppSettingHelper } from '@affine/core/hooks/affine/use-app-setting-helper';
import { RecentPagesService } from '@affine/core/modules/cmdk';
import type { PageRootService } from '@blocksuite/blocks';
@ -65,8 +69,6 @@ import { PageNotFound } from '../../404';
import * as styles from './detail-page.css';
import { DetailPageHeader } from './detail-page-header';
const RIGHT_SIDEBAR_TABS_ACTIVE_KEY = 'app:settings:rightsidebar:tabs:active';
const DetailPageImpl = memo(function DetailPageImpl() {
const globalState = useService(GlobalStateService).globalState;
const activeTabName = useLiveData(
@ -237,6 +239,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
</ViewHeaderIsland>
<ViewBodyIsland>
<div className={styles.mainContainer}>
<AIIsland />
{/* Add a key to force rerender when page changed, to avoid error boundary persisting. */}
<AffineErrorBoundary key={doc.id}>
<TopTip pageId={doc.id} workspace={workspace} />

View File

@ -0,0 +1,16 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import {
clickNewPageButton,
waitForEditorLoad,
} from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('Click ai-land icon', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await clickNewPageButton(page);
await page.locator('[data-testid=ai-island]').click();
await expect(page.locator('chat-panel')).toBeVisible();
});

View File

@ -1,17 +0,0 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('Click right-bottom corner contact icon', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await page.locator('[data-testid=help-island]').click();
const rightBottomContactUs = page.getByTestId('right-bottom-contact-us-icon');
await expect(rightBottomContactUs).toBeVisible();
await rightBottomContactUs.click();
const title = page.getByTestId('about-title');
await expect(title).toBeVisible();
});

View File

@ -1,23 +0,0 @@
import { test } from '@affine-test/kit/playwright';
import { openHomePage } from '@affine-test/kit/utils/load-page';
import { waitForEditorLoad } from '@affine-test/kit/utils/page-logic';
import { expect } from '@playwright/test';
test('Open shortcuts modal', async ({ page }) => {
await openHomePage(page);
await waitForEditorLoad(page);
await page.locator('[data-testid=help-island]').click();
const shortcutsIcon = page.locator('[data-testid=shortcuts-icon]');
await page.waitForTimeout(1000);
await expect(shortcutsIcon).toBeVisible();
await shortcutsIcon.click();
await page.waitForTimeout(1000);
const settingModal = page.getByTestId('setting-modal');
await expect(settingModal).toBeVisible();
const title = page.getByTestId('keyboard-shortcuts-title');
await expect(title).toBeVisible();
});