feat(core): ai onboarding for edgeless mode (#6556)

This commit is contained in:
CatsJuice
2024-04-15 07:25:37 +00:00
parent 257e946d5d
commit b93e79c59d
14 changed files with 88359 additions and 52 deletions
@@ -15,20 +15,19 @@ import {
export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> { export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
notification: Notification; notification: Notification;
onDismiss?: () => void;
} }
export const NotificationCard = ({ export const NotificationCard = ({ notification }: NotificationCardProps) => {
notification,
onDismiss,
}: NotificationCardProps) => {
const { const {
theme = 'info', theme = 'info',
style = 'normal', style = 'normal',
icon = <InformationFillDuotoneIcon />, icon = <InformationFillDuotoneIcon />,
thumb,
action, action,
title, title,
footer, footer,
alignMessage = 'title',
onDismiss,
} = notification; } = notification;
const onActionClicked = useCallback(() => { const onActionClicked = useCallback(() => {
@@ -49,33 +48,41 @@ export const NotificationCard = ({
data-with-icon={icon ? '' : undefined} data-with-icon={icon ? '' : undefined}
className={styles.card} className={styles.card}
> >
<header className={styles.header}> {thumb}
{icon ? ( <div className={styles.cardInner}>
<div className={clsx(styles.icon, styles.headAlignWrapper)}> <header className={styles.header}>
{icon} {icon ? (
</div> <div className={clsx(styles.icon, styles.headAlignWrapper)}>
) : null} {icon}
<div className={styles.title}>{title}</div> </div>
) : null}
<div className={styles.title}>{title}</div>
{action ? ( {action ? (
<div className={clsx(styles.headAlignWrapper, styles.action)}> <div className={clsx(styles.headAlignWrapper, styles.action)}>
<Button <Button
className={styles.actionButton} className={styles.actionButton}
onClick={onActionClicked} onClick={onActionClicked}
{...action.buttonProps} {...action.buttonProps}
> >
{action.label} {action.label}
</Button> </Button>
</div>
) : null}
<div
data-float={!!thumb}
className={clsx(styles.headAlignWrapper, styles.closeButton)}
>
<IconButton onClick={onDismiss}>
<CloseIcon className={styles.closeIcon} width={16} height={16} />
</IconButton>
</div> </div>
) : null} </header>
<div className={styles.headAlignWrapper}> <main data-align={alignMessage} className={styles.main}>
<IconButton onClick={onDismiss}> {notification.message}
<CloseIcon className={styles.closeIcon} width={16} height={16} /> </main>
</IconButton> <footer>{footer}</footer>
</div> </div>
</header>
<main className={styles.main}>{notification.message}</main>
<footer>{footer}</footer>
</div> </div>
); );
}; };
@@ -46,12 +46,11 @@ export function NotificationCenter({ width = 380 }: NotificationCenterProps) {
*/ */
export function notify(notification: Notification, options?: ExternalToast) { export function notify(notification: Notification, options?: ExternalToast) {
return toast.custom(id => { return toast.custom(id => {
return ( const onDismiss = () => {
<NotificationCard notification.onDismiss?.();
notification={notification} toast.dismiss(id);
onDismiss={() => toast.dismiss(id)} };
/> return <NotificationCard notification={{ ...notification, onDismiss }} />;
);
}, options); }, options);
} }
@@ -10,12 +10,14 @@ export const card = style({
borderRadius: 8, borderRadius: 8,
borderWidth: 1, borderWidth: 1,
borderStyle: 'solid', borderStyle: 'solid',
padding: 16,
boxShadow: cssVar('shadow1'), boxShadow: cssVar('shadow1'),
backgroundColor: cardColor, backgroundColor: cardColor,
borderColor: cardBorderColor, borderColor: cardBorderColor,
color: cardForeground, color: cardForeground,
}); });
export const cardInner = style({
padding: 16,
});
export const header = style({ export const header = style({
display: 'flex', display: 'flex',
@@ -68,17 +70,26 @@ export const actionButton = style({
boxShadow: 'none !important', boxShadow: 'none !important',
}, },
}); });
export const closeButton = style({
selectors: {
'&[data-float="true"]': {
position: 'absolute',
top: 16,
right: 16,
},
},
});
export const closeIcon = style({ export const closeIcon = style({
color: `${cardForeground} !important`, color: `${cardForeground} !important`,
}); });
export const main = style({ export const main = style({
marginTop: 5, marginTop: 5,
fontSize: 14, fontSize: cssVar('fontSm'),
lineHeight: '22px', lineHeight: '22px',
selectors: { selectors: {
'[data-with-icon] &': { '[data-with-icon] &[data-align="title"]': {
paddingLeft: 34, paddingLeft: 34,
}, },
}, },
@@ -12,6 +12,7 @@ export interface Notification {
borderColor?: string; borderColor?: string;
background?: string; background?: string;
foreground?: string; foreground?: string;
alignMessage?: 'title' | 'icon';
action?: { action?: {
label: string; label: string;
onClick: (() => void) | (() => Promise<void>); onClick: (() => void) | (() => Promise<void>);
@@ -23,10 +24,14 @@ export interface Notification {
}; };
// custom slots // custom slots
thumb?: ReactNode;
title?: ReactNode; title?: ReactNode;
message?: ReactNode; message?: ReactNode;
icon?: ReactNode; icon?: ReactNode;
footer?: ReactNode; footer?: ReactNode;
// events
onDismiss?: () => void;
} }
export interface NotificationCenterProps { export interface NotificationCenterProps {
@@ -0,0 +1,12 @@
import { cssVar } from '@toeverything/theme';
import { style } from '@vanilla-extract/css';
export const thumb = style({
borderRadius: 'inherit',
borderBottomLeftRadius: 0,
borderBottomRightRadius: 0,
width: '100%',
height: 211,
background: cssVar('backgroundOverlayPanelColor'),
overflow: 'hidden',
});
@@ -1,9 +1,73 @@
import { notify } from '@affine/component';
import { CurrentWorkspaceService } from '@affine/core/modules/workspace';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { AiIcon } from '@blocksuite/icons';
import { Doc, LiveData, useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import Lottie from 'lottie-react';
import { useTheme } from 'next-themes';
import { useEffect, useMemo, useRef } from 'react';
import * as styles from './edgeless.dialog.css';
import mouseDark from './lottie/edgeless/mouse-dark.json';
import mouseLight from './lottie/edgeless/mouse-light.json';
import trackPadDark from './lottie/edgeless/trackpad-dark.json';
import trackPadLight from './lottie/edgeless/trackpad-light.json';
import type { BaseAIOnboardingDialogProps } from './type'; import type { BaseAIOnboardingDialogProps } from './type';
const EdgelessOnboardingAnimation = () => {
const { resolvedTheme } = useTheme();
const isTrackPad = false;
const data = useMemo(() => {
if (isTrackPad) {
return resolvedTheme === 'dark' ? trackPadDark : trackPadLight;
}
return resolvedTheme === 'dark' ? mouseDark : mouseLight;
}, [isTrackPad, resolvedTheme]);
return <Lottie loop autoplay animationData={data} className={styles.thumb} />;
};
// avoid notifying multiple times
const notifyId$ = new LiveData<string | number | null>(null);
export const AIOnboardingEdgeless = ({ export const AIOnboardingEdgeless = ({
onDismiss: _, onDismiss,
}: BaseAIOnboardingDialogProps) => { }: BaseAIOnboardingDialogProps) => {
return ( const t = useAFFiNEI18N();
<div>{/* TODO: open edgeless in cloud workspace for the first time */}</div> const notifyId = useLiveData(notifyId$);
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
const currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace$
); );
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
const doc = useService(Doc);
const mode = useLiveData(doc.mode$);
useEffect(() => {
if (notifyId) return;
if (isCloud && mode === 'edgeless') {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
const id = notify(
{
title: t['com.affine.ai-onboarding.edgeless.title'](),
message: t['com.affine.ai-onboarding.edgeless.message'](),
icon: <AiIcon color={cssVar('processingColor')} />,
thumb: <EdgelessOnboardingAnimation />,
alignMessage: 'icon',
onDismiss,
},
{ duration: 1000 * 60 * 10 }
);
notifyId$.next(id);
}, 1000);
}
}, [isCloud, mode, notifyId, onDismiss, t]);
return null;
}; };
@@ -16,13 +16,10 @@ const useDismiss = (key: AIOnboardingType) => {
return [dismiss, onDismiss] as const; return [dismiss, onDismiss] as const;
}; };
export const AIOnboarding = () => { export const WorkspaceAIOnboarding = () => {
const [dismissGeneral, onDismissGeneral] = useDismiss( const [dismissGeneral, onDismissGeneral] = useDismiss(
AIOnboardingType.GENERAL AIOnboardingType.GENERAL
); );
const [dismissEdgeless, onDismissEdgeless] = useDismiss(
AIOnboardingType.EDGELESS
);
const [dismissLocal, onDismissLocal] = useDismiss(AIOnboardingType.LOCAL); const [dismissLocal, onDismissLocal] = useDismiss(AIOnboardingType.LOCAL);
return ( return (
@@ -30,10 +27,22 @@ export const AIOnboarding = () => {
{dismissGeneral ? null : ( {dismissGeneral ? null : (
<AIOnboardingGeneral onDismiss={onDismissGeneral} /> <AIOnboardingGeneral onDismiss={onDismissGeneral} />
)} )}
{dismissEdgeless ? null : (
<AIOnboardingEdgeless onDismiss={onDismissEdgeless} />
)}
{dismissLocal ? null : <AIOnboardingLocal onDismiss={onDismissLocal} />} {dismissLocal ? null : <AIOnboardingLocal onDismiss={onDismissLocal} />}
</Suspense> </Suspense>
); );
}; };
export const PageAIOnboarding = () => {
const [dismissEdgeless, onDismissEdgeless] = useDismiss(
AIOnboardingType.EDGELESS
);
return (
<Suspense>
{dismissEdgeless ? null : (
<AIOnboardingEdgeless onDismiss={onDismissEdgeless} />
)}
</Suspense>
);
};
@@ -22,7 +22,7 @@ import { matchPath } from 'react-router-dom';
import { Map as YMap } from 'yjs'; import { Map as YMap } from 'yjs';
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms'; import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
import { AIOnboarding } from '../components/affine/ai-onboarding'; import { WorkspaceAIOnboarding } from '../components/affine/ai-onboarding';
import { AppContainer } from '../components/affine/app-container'; import { AppContainer } from '../components/affine/app-container';
import { SyncAwareness } from '../components/affine/awareness'; import { SyncAwareness } from '../components/affine/awareness';
import { import {
@@ -102,7 +102,7 @@ export const WorkspaceLayout = function WorkspaceLayout({
<Suspense fallback={<WorkspaceFallback />}> <Suspense fallback={<WorkspaceFallback />}>
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner> <WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
{/* should show after workspace loaded */} {/* should show after workspace loaded */}
<AIOnboarding /> <WorkspaceAIOnboarding />
</Suspense> </Suspense>
</SWRConfigProvider> </SWRConfigProvider>
); );
@@ -1,5 +1,6 @@
import { Scrollable } from '@affine/component'; import { Scrollable } from '@affine/component';
import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton'; import { PageDetailSkeleton } from '@affine/component/page-detail-skeleton';
import { PageAIOnboarding } from '@affine/core/components/affine/ai-onboarding';
import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta'; import { useBlockSuiteDocMeta } from '@affine/core/hooks/use-block-suite-page-meta';
import type { PageRootService } from '@blocksuite/blocks'; import type { PageRootService } from '@blocksuite/blocks';
import { import {
@@ -283,6 +284,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
<ImagePreviewModal pageId={currentPageId} docCollection={docCollection} /> <ImagePreviewModal pageId={currentPageId} docCollection={docCollection} />
<GlobalPageHistoryModal /> <GlobalPageHistoryModal />
<PageAIOnboarding />
</> </>
); );
}); });
+3 -1
View File
@@ -1294,5 +1294,7 @@
"com.affine.ai-onboarding.general.next": "Next", "com.affine.ai-onboarding.general.next": "Next",
"com.affine.ai-onboarding.general.prev": "Back", "com.affine.ai-onboarding.general.prev": "Back",
"com.affine.ai-onboarding.general.try-for-free": "Tree for Free", "com.affine.ai-onboarding.general.try-for-free": "Tree for Free",
"com.affine.ai-onboarding.general.purchase": "Get Unlimited Usage" "com.affine.ai-onboarding.general.purchase": "Get Unlimited Usage",
"com.affine.ai-onboarding.edgeless.title": "Meet AFFiNE AI",
"com.affine.ai-onboarding.edgeless.message": "Lets you think bigger, create faster, work smarter and save time for every project."
} }