mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-13 16:16:46 +08:00
feat(core): ai onboarding for edgeless mode (#6556)
This commit is contained in:
@@ -15,20 +15,19 @@ import {
|
||||
|
||||
export interface NotificationCardProps extends HTMLAttributes<HTMLDivElement> {
|
||||
notification: Notification;
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
export const NotificationCard = ({
|
||||
notification,
|
||||
onDismiss,
|
||||
}: NotificationCardProps) => {
|
||||
export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
||||
const {
|
||||
theme = 'info',
|
||||
style = 'normal',
|
||||
icon = <InformationFillDuotoneIcon />,
|
||||
thumb,
|
||||
action,
|
||||
title,
|
||||
footer,
|
||||
alignMessage = 'title',
|
||||
onDismiss,
|
||||
} = notification;
|
||||
|
||||
const onActionClicked = useCallback(() => {
|
||||
@@ -49,33 +48,41 @@ export const NotificationCard = ({
|
||||
data-with-icon={icon ? '' : undefined}
|
||||
className={styles.card}
|
||||
>
|
||||
<header className={styles.header}>
|
||||
{icon ? (
|
||||
<div className={clsx(styles.icon, styles.headAlignWrapper)}>
|
||||
{icon}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.title}>{title}</div>
|
||||
{thumb}
|
||||
<div className={styles.cardInner}>
|
||||
<header className={styles.header}>
|
||||
{icon ? (
|
||||
<div className={clsx(styles.icon, styles.headAlignWrapper)}>
|
||||
{icon}
|
||||
</div>
|
||||
) : null}
|
||||
<div className={styles.title}>{title}</div>
|
||||
|
||||
{action ? (
|
||||
<div className={clsx(styles.headAlignWrapper, styles.action)}>
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
onClick={onActionClicked}
|
||||
{...action.buttonProps}
|
||||
>
|
||||
{action.label}
|
||||
</Button>
|
||||
{action ? (
|
||||
<div className={clsx(styles.headAlignWrapper, styles.action)}>
|
||||
<Button
|
||||
className={styles.actionButton}
|
||||
onClick={onActionClicked}
|
||||
{...action.buttonProps}
|
||||
>
|
||||
{action.label}
|
||||
</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>
|
||||
) : null}
|
||||
<div className={styles.headAlignWrapper}>
|
||||
<IconButton onClick={onDismiss}>
|
||||
<CloseIcon className={styles.closeIcon} width={16} height={16} />
|
||||
</IconButton>
|
||||
</div>
|
||||
</header>
|
||||
<main className={styles.main}>{notification.message}</main>
|
||||
<footer>{footer}</footer>
|
||||
</header>
|
||||
<main data-align={alignMessage} 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) {
|
||||
return toast.custom(id => {
|
||||
return (
|
||||
<NotificationCard
|
||||
notification={notification}
|
||||
onDismiss={() => toast.dismiss(id)}
|
||||
/>
|
||||
);
|
||||
const onDismiss = () => {
|
||||
notification.onDismiss?.();
|
||||
toast.dismiss(id);
|
||||
};
|
||||
return <NotificationCard notification={{ ...notification, onDismiss }} />;
|
||||
}, options);
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,14 @@ export const card = style({
|
||||
borderRadius: 8,
|
||||
borderWidth: 1,
|
||||
borderStyle: 'solid',
|
||||
padding: 16,
|
||||
boxShadow: cssVar('shadow1'),
|
||||
backgroundColor: cardColor,
|
||||
borderColor: cardBorderColor,
|
||||
color: cardForeground,
|
||||
});
|
||||
export const cardInner = style({
|
||||
padding: 16,
|
||||
});
|
||||
|
||||
export const header = style({
|
||||
display: 'flex',
|
||||
@@ -68,17 +70,26 @@ export const actionButton = style({
|
||||
boxShadow: 'none !important',
|
||||
},
|
||||
});
|
||||
export const closeButton = style({
|
||||
selectors: {
|
||||
'&[data-float="true"]': {
|
||||
position: 'absolute',
|
||||
top: 16,
|
||||
right: 16,
|
||||
},
|
||||
},
|
||||
});
|
||||
export const closeIcon = style({
|
||||
color: `${cardForeground} !important`,
|
||||
});
|
||||
|
||||
export const main = style({
|
||||
marginTop: 5,
|
||||
fontSize: 14,
|
||||
fontSize: cssVar('fontSm'),
|
||||
lineHeight: '22px',
|
||||
|
||||
selectors: {
|
||||
'[data-with-icon] &': {
|
||||
'[data-with-icon] &[data-align="title"]': {
|
||||
paddingLeft: 34,
|
||||
},
|
||||
},
|
||||
|
||||
@@ -12,6 +12,7 @@ export interface Notification {
|
||||
borderColor?: string;
|
||||
background?: string;
|
||||
foreground?: string;
|
||||
alignMessage?: 'title' | 'icon';
|
||||
action?: {
|
||||
label: string;
|
||||
onClick: (() => void) | (() => Promise<void>);
|
||||
@@ -23,10 +24,14 @@ export interface Notification {
|
||||
};
|
||||
|
||||
// custom slots
|
||||
thumb?: ReactNode;
|
||||
title?: ReactNode;
|
||||
message?: ReactNode;
|
||||
icon?: ReactNode;
|
||||
footer?: ReactNode;
|
||||
|
||||
// events
|
||||
onDismiss?: () => void;
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
export const AIOnboardingEdgeless = ({
|
||||
onDismiss: _,
|
||||
}: BaseAIOnboardingDialogProps) => {
|
||||
return (
|
||||
<div>{/* TODO: open edgeless in cloud workspace for the first time */}</div>
|
||||
);
|
||||
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 = ({
|
||||
onDismiss,
|
||||
}: BaseAIOnboardingDialogProps) => {
|
||||
const t = useAFFiNEI18N();
|
||||
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;
|
||||
};
|
||||
|
||||
export const AIOnboarding = () => {
|
||||
export const WorkspaceAIOnboarding = () => {
|
||||
const [dismissGeneral, onDismissGeneral] = useDismiss(
|
||||
AIOnboardingType.GENERAL
|
||||
);
|
||||
const [dismissEdgeless, onDismissEdgeless] = useDismiss(
|
||||
AIOnboardingType.EDGELESS
|
||||
);
|
||||
const [dismissLocal, onDismissLocal] = useDismiss(AIOnboardingType.LOCAL);
|
||||
|
||||
return (
|
||||
@@ -30,10 +27,22 @@ export const AIOnboarding = () => {
|
||||
{dismissGeneral ? null : (
|
||||
<AIOnboardingGeneral onDismiss={onDismissGeneral} />
|
||||
)}
|
||||
{dismissEdgeless ? null : (
|
||||
<AIOnboardingEdgeless onDismiss={onDismissEdgeless} />
|
||||
)}
|
||||
|
||||
{dismissLocal ? null : <AIOnboardingLocal onDismiss={onDismissLocal} />}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
export const PageAIOnboarding = () => {
|
||||
const [dismissEdgeless, onDismissEdgeless] = useDismiss(
|
||||
AIOnboardingType.EDGELESS
|
||||
);
|
||||
|
||||
return (
|
||||
<Suspense>
|
||||
{dismissEdgeless ? null : (
|
||||
<AIOnboardingEdgeless onDismiss={onDismissEdgeless} />
|
||||
)}
|
||||
</Suspense>
|
||||
);
|
||||
};
|
||||
|
||||
+22106
File diff suppressed because it is too large
Load Diff
+22106
File diff suppressed because it is too large
Load Diff
+21981
File diff suppressed because it is too large
Load Diff
+22003
File diff suppressed because it is too large
Load Diff
@@ -22,7 +22,7 @@ import { matchPath } from 'react-router-dom';
|
||||
import { Map as YMap } from 'yjs';
|
||||
|
||||
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 { SyncAwareness } from '../components/affine/awareness';
|
||||
import {
|
||||
@@ -102,7 +102,7 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
||||
<Suspense fallback={<WorkspaceFallback />}>
|
||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||
{/* should show after workspace loaded */}
|
||||
<AIOnboarding />
|
||||
<WorkspaceAIOnboarding />
|
||||
</Suspense>
|
||||
</SWRConfigProvider>
|
||||
);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Scrollable } from '@affine/component';
|
||||
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 type { PageRootService } from '@blocksuite/blocks';
|
||||
import {
|
||||
@@ -283,6 +284,7 @@ const DetailPageImpl = memo(function DetailPageImpl() {
|
||||
|
||||
<ImagePreviewModal pageId={currentPageId} docCollection={docCollection} />
|
||||
<GlobalPageHistoryModal />
|
||||
<PageAIOnboarding />
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1294,5 +1294,7 @@
|
||||
"com.affine.ai-onboarding.general.next": "Next",
|
||||
"com.affine.ai-onboarding.general.prev": "Back",
|
||||
"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."
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user