mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 02:13:00 +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>
|
||||
);
|
||||
};
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
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