mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-22 20:46:38 +08:00
feat(core): adjust ai help island style and behavior, add animation (#7310)
- Move right-sidebar activeTab state into `RightSidebarService` - Remove `styled` usage and adjust the UI - Fix the issue that ai-button clicking not work when sidebar opened - Add an animation if AI-Chat panel hasn't been opened. 
This commit is contained in:
@@ -1,65 +1,66 @@
|
||||
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 { useLiveData, useService } from '@toeverything/infra';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { ToolContainer } from '../../workspace';
|
||||
import { AIIcon } from './icons';
|
||||
import { StyledIsland, StyledTriggerWrapper } from './style';
|
||||
import {
|
||||
aiIslandAnimationBg,
|
||||
aiIslandBtn,
|
||||
aiIslandWrapper,
|
||||
borderAngle1,
|
||||
borderAngle2,
|
||||
borderAngle3,
|
||||
} from './styles.css';
|
||||
|
||||
export const RIGHT_SIDEBAR_TABS_ACTIVE_KEY =
|
||||
'app:settings:rightsidebar:tabs:active';
|
||||
if (typeof window !== 'undefined' && window.CSS) {
|
||||
const getName = (nameWithVar: string) => nameWithVar.slice(4, -1);
|
||||
const registerAngle = (varName: string, initialValue: number) => {
|
||||
window.CSS.registerProperty({
|
||||
name: getName(varName),
|
||||
syntax: '<angle>',
|
||||
inherits: false,
|
||||
initialValue: `${initialValue}deg`,
|
||||
});
|
||||
};
|
||||
registerAngle(borderAngle1, 0);
|
||||
registerAngle(borderAngle2, 90);
|
||||
registerAngle(borderAngle3, 180);
|
||||
}
|
||||
|
||||
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$);
|
||||
// to make sure ai island is hidden first and animate in
|
||||
const [hide, setHide] = useState(true);
|
||||
|
||||
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 activeTabName = useLiveData(rightSidebar.activeTabName$);
|
||||
const rightSidebarOpen = useLiveData(rightSidebar.isOpen$);
|
||||
const aiChatHasEverOpened = useLiveData(rightSidebar.aiChatHasEverOpened$);
|
||||
|
||||
useEffect(() => {
|
||||
setHide(rightSidebarOpen && activeTabName === 'chat');
|
||||
}, [activeTabName, rightSidebarOpen]);
|
||||
|
||||
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'}
|
||||
<div
|
||||
className={aiIslandWrapper}
|
||||
data-hide={hide}
|
||||
data-animation={!aiChatHasEverOpened}
|
||||
>
|
||||
<StyledTriggerWrapper data-testid="faq-icon">
|
||||
<div className={aiIslandAnimationBg} />
|
||||
<button
|
||||
className={aiIslandBtn}
|
||||
data-testid="ai-island"
|
||||
onClick={() => {
|
||||
if (hide) return;
|
||||
rightSidebar.open();
|
||||
rightSidebar.setActiveTabName('chat');
|
||||
}}
|
||||
>
|
||||
<AIIcon />
|
||||
</StyledTriggerWrapper>
|
||||
</StyledIsland>
|
||||
</button>
|
||||
</div>
|
||||
</ToolContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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,
|
||||
},
|
||||
};
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { createVar, keyframes, style } from '@vanilla-extract/css';
|
||||
|
||||
export const aiIslandWrapper = style({
|
||||
width: 44,
|
||||
height: 44,
|
||||
position: 'relative',
|
||||
transform: 'translateY(0)',
|
||||
transition: 'transform 0.2s ease',
|
||||
|
||||
selectors: {
|
||||
'&[data-hide="true"]': {
|
||||
transform: 'translateY(120px)',
|
||||
transitionDelay: '0.2s',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const aiIslandBtn = style({
|
||||
width: 'inherit',
|
||||
height: 'inherit',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
borderRadius: '50%',
|
||||
color: cssVar('iconColor'),
|
||||
border: `0.5px solid ${cssVar('borderColor')}`,
|
||||
boxShadow: '0px 2px 2px rgba(0,0,0,0.05)',
|
||||
background: cssVar('backgroundOverlayPanelColor'),
|
||||
position: 'relative',
|
||||
|
||||
selectors: {
|
||||
[`${aiIslandWrapper}[data-animation="true"] &`]: {
|
||||
borderColor: 'transparent',
|
||||
},
|
||||
'&:hover::after': {
|
||||
content: '""',
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
borderRadius: '50%',
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// -------- animation --------
|
||||
export const borderAngle1 = createVar('border-angle-1');
|
||||
export const borderAngle2 = createVar('border-angle-2');
|
||||
export const borderAngle3 = createVar('border-angle-3');
|
||||
const brightBlue = createVar('bright-blue');
|
||||
const brightGreen = createVar('bright-green');
|
||||
const brightRed = createVar('bright-red');
|
||||
const borderWidth = createVar('border-width');
|
||||
|
||||
const rotateBg1 = keyframes({
|
||||
to: { [borderAngle1.slice(4, -1)]: '360deg' },
|
||||
});
|
||||
const rotateBg2 = keyframes({
|
||||
to: { [borderAngle2.slice(4, -1)]: '450deg' },
|
||||
});
|
||||
const rotateBg3 = keyframes({
|
||||
to: { [borderAngle3.slice(4, -1)]: '540deg' },
|
||||
});
|
||||
|
||||
export const aiIslandAnimationBg = style({
|
||||
width: 'inherit',
|
||||
height: 'inherit',
|
||||
top: 0,
|
||||
left: 0,
|
||||
position: 'absolute',
|
||||
borderRadius: '50%',
|
||||
|
||||
vars: {
|
||||
[borderAngle1]: '0deg',
|
||||
[borderAngle2]: '90deg',
|
||||
[borderAngle3]: '180deg',
|
||||
[brightBlue]: 'rgb(0, 100, 255)',
|
||||
[brightGreen]: '#1E96EB',
|
||||
[brightRed]: 'rgb(0, 200, 255)',
|
||||
[borderWidth]: '1.5px',
|
||||
},
|
||||
backgroundColor: 'transparent',
|
||||
backgroundImage: `conic-gradient(from ${borderAngle1} at 50% 50%,
|
||||
transparent,
|
||||
${brightBlue} 10%,
|
||||
transparent 30%,
|
||||
transparent),
|
||||
conic-gradient(from ${borderAngle2} at 50% 50%,
|
||||
transparent,
|
||||
${brightGreen} 10%,
|
||||
transparent 60%,
|
||||
transparent),
|
||||
conic-gradient(from ${borderAngle3} at 50% 50%,
|
||||
transparent,
|
||||
${brightRed} 10%,
|
||||
transparent 50%,
|
||||
transparent)`,
|
||||
|
||||
selectors: {
|
||||
[`${aiIslandWrapper}[data-animation="true"] &`]: {
|
||||
width: `calc(100% + 2 * ${borderWidth})`,
|
||||
height: `calc(100% + 2 * ${borderWidth})`,
|
||||
top: `calc(-1 * ${borderWidth})`,
|
||||
left: `calc(-1 * ${borderWidth})`,
|
||||
animation: `${rotateBg1} 3s linear infinite, ${rotateBg2} 8s linear infinite, ${rotateBg3} 13s linear infinite`,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -87,27 +87,16 @@ export const mainContainerStyle = style({
|
||||
});
|
||||
export const toolStyle = style({
|
||||
position: 'absolute',
|
||||
right: '30px',
|
||||
bottom: '30px',
|
||||
right: 16,
|
||||
bottom: 16,
|
||||
zIndex: 1,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
alignItems: 'center',
|
||||
gap: '12px',
|
||||
selectors: {
|
||||
'&.trash': {
|
||||
bottom: '78px',
|
||||
},
|
||||
},
|
||||
'@media': {
|
||||
'screen and (max-width: 960px)': {
|
||||
right: 'calc((100vw - 640px) * 3 / 19 + 14px)',
|
||||
},
|
||||
'screen and (max-width: 640px)': {
|
||||
right: '5px',
|
||||
bottom: '5px',
|
||||
},
|
||||
print: {
|
||||
display: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user