mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-09-22 03:33:06 +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> {
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
+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 { 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 />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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."
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user