feat(core): optimize ai onboarding trigger logic (#6579)
- don't open edgeless ai-onboarding dialog until general ai onboarding and setting modal closed - clip edgeless ai onboarding thumb to avoid "black border" - correct "try for free" - replace edgeless ai onboarding lottie resources
This commit is contained in:
@@ -9,4 +9,13 @@ export const thumb = style({
|
|||||||
height: 211,
|
height: 211,
|
||||||
background: cssVar('backgroundOverlayPanelColor'),
|
background: cssVar('backgroundOverlayPanelColor'),
|
||||||
overflow: 'hidden',
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const thumbContent = style({
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
width: 'calc(100% + 4px)',
|
||||||
|
height: 'calc(100% + 4px)',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,44 +1,48 @@
|
|||||||
import { notify } from '@affine/component';
|
import { notify } from '@affine/component';
|
||||||
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { CurrentWorkspaceService } from '@affine/core/modules/workspace';
|
import { CurrentWorkspaceService } from '@affine/core/modules/workspace';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { AiIcon } from '@blocksuite/icons';
|
import { AiIcon } from '@blocksuite/icons';
|
||||||
import { Doc, LiveData, useLiveData, useService } from '@toeverything/infra';
|
import { Doc, useLiveData, useService } from '@toeverything/infra';
|
||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { useAtomValue } from 'jotai';
|
||||||
import Lottie from 'lottie-react';
|
import Lottie from 'lottie-react';
|
||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useEffect, useMemo, useRef } from 'react';
|
import { useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
import * as styles from './edgeless.dialog.css';
|
import * as styles from './edgeless.dialog.css';
|
||||||
import mouseDark from './lottie/edgeless/mouse-dark.json';
|
import mouseTrackDark from './lottie/edgeless/mouse-track-dark.json';
|
||||||
import mouseLight from './lottie/edgeless/mouse-light.json';
|
import mouseTrackLight from './lottie/edgeless/mouse-track-light.json';
|
||||||
import trackPadDark from './lottie/edgeless/trackpad-dark.json';
|
import { edgelessNotifyId$, showAIOnboardingGeneral$ } from './state';
|
||||||
import trackPadLight from './lottie/edgeless/trackpad-light.json';
|
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
import type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
const EdgelessOnboardingAnimation = () => {
|
const EdgelessOnboardingAnimation = () => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
|
|
||||||
const isTrackPad = false;
|
|
||||||
|
|
||||||
const data = useMemo(() => {
|
const data = useMemo(() => {
|
||||||
if (isTrackPad) {
|
return resolvedTheme === 'dark' ? mouseTrackDark : mouseTrackLight;
|
||||||
return resolvedTheme === 'dark' ? trackPadDark : trackPadLight;
|
}, [resolvedTheme]);
|
||||||
}
|
|
||||||
return resolvedTheme === 'dark' ? mouseDark : mouseLight;
|
|
||||||
}, [isTrackPad, resolvedTheme]);
|
|
||||||
|
|
||||||
return <Lottie loop autoplay animationData={data} className={styles.thumb} />;
|
return (
|
||||||
|
<div className={styles.thumb}>
|
||||||
|
<Lottie
|
||||||
|
loop
|
||||||
|
autoplay
|
||||||
|
animationData={data}
|
||||||
|
className={styles.thumbContent}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
// avoid notifying multiple times
|
|
||||||
const notifyId$ = new LiveData<string | number | null>(null);
|
|
||||||
|
|
||||||
export const AIOnboardingEdgeless = ({
|
export const AIOnboardingEdgeless = ({
|
||||||
onDismiss,
|
onDismiss,
|
||||||
}: BaseAIOnboardingDialogProps) => {
|
}: BaseAIOnboardingDialogProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const notifyId = useLiveData(notifyId$);
|
const notifyId = useLiveData(edgelessNotifyId$);
|
||||||
|
const generalAIOnboardingOpened = useLiveData(showAIOnboardingGeneral$);
|
||||||
|
const settingModalOpen = useAtomValue(openSettingModalAtom);
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const currentWorkspace = useLiveData(
|
const currentWorkspace = useLiveData(
|
||||||
useService(CurrentWorkspaceService).currentWorkspace$
|
useService(CurrentWorkspaceService).currentWorkspace$
|
||||||
@@ -49,6 +53,8 @@ export const AIOnboardingEdgeless = ({
|
|||||||
const mode = useLiveData(doc.mode$);
|
const mode = useLiveData(doc.mode$);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (settingModalOpen.open) return;
|
||||||
|
if (generalAIOnboardingOpened) return;
|
||||||
if (notifyId) return;
|
if (notifyId) return;
|
||||||
if (isCloud && mode === 'edgeless') {
|
if (isCloud && mode === 'edgeless') {
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
@@ -64,10 +70,18 @@ export const AIOnboardingEdgeless = ({
|
|||||||
},
|
},
|
||||||
{ duration: 1000 * 60 * 10 }
|
{ duration: 1000 * 60 * 10 }
|
||||||
);
|
);
|
||||||
notifyId$.next(id);
|
edgelessNotifyId$.next(id);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
}, [isCloud, mode, notifyId, onDismiss, t]);
|
}, [
|
||||||
|
generalAIOnboardingOpened,
|
||||||
|
isCloud,
|
||||||
|
mode,
|
||||||
|
notifyId,
|
||||||
|
onDismiss,
|
||||||
|
settingModalOpen,
|
||||||
|
t,
|
||||||
|
]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|||||||
import * as baseStyles from './base-style.css';
|
import * as baseStyles from './base-style.css';
|
||||||
import * as styles from './general.dialog.css';
|
import * as styles from './general.dialog.css';
|
||||||
import { Slider } from './slider';
|
import { Slider } from './slider';
|
||||||
|
import { showAIOnboardingGeneral$ } from './state';
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
import type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
type PlayListItem = { video: string; title: ReactNode; desc: ReactNode };
|
type PlayListItem = { video: string; title: ReactNode; desc: ReactNode };
|
||||||
@@ -71,7 +72,8 @@ export const AIOnboardingGeneral = ({
|
|||||||
);
|
);
|
||||||
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const [open, setOpen] = useState(true);
|
// const [open, setOpen] = useState(true);
|
||||||
|
const open = useLiveData(showAIOnboardingGeneral$);
|
||||||
const [index, setIndex] = useState(0);
|
const [index, setIndex] = useState(0);
|
||||||
const list = useMemo(() => getPlayList(t), [t]);
|
const list = useMemo(() => getPlayList(t), [t]);
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
@@ -81,7 +83,7 @@ export const AIOnboardingGeneral = ({
|
|||||||
const isLast = index === list.length - 1;
|
const isLast = index === list.length - 1;
|
||||||
|
|
||||||
const closeAndDismiss = useCallback(() => {
|
const closeAndDismiss = useCallback(() => {
|
||||||
setOpen(false);
|
showAIOnboardingGeneral$.next(false);
|
||||||
onDismiss();
|
onDismiss();
|
||||||
}, [onDismiss]);
|
}, [onDismiss]);
|
||||||
const goToPricingPlans = useCallback(() => {
|
const goToPricingPlans = useCallback(() => {
|
||||||
@@ -92,7 +94,7 @@ export const AIOnboardingGeneral = ({
|
|||||||
});
|
});
|
||||||
closeAndDismiss();
|
closeAndDismiss();
|
||||||
}, [closeAndDismiss, setSettingModal]);
|
}, [closeAndDismiss, setSettingModal]);
|
||||||
const onClose = useCallback(() => setOpen(false), []);
|
const onClose = useCallback(() => showAIOnboardingGeneral$.next(false), []);
|
||||||
const onPrev = useCallback(() => {
|
const onPrev = useCallback(() => {
|
||||||
setIndex(i => Math.max(0, i - 1));
|
setIndex(i => Math.max(0, i - 1));
|
||||||
}, []);
|
}, []);
|
||||||
@@ -101,9 +103,16 @@ export const AIOnboardingGeneral = ({
|
|||||||
}, [list.length]);
|
}, [list.length]);
|
||||||
|
|
||||||
const videoRenderer = useCallback(
|
const videoRenderer = useCallback(
|
||||||
({ video }: PlayListItem) => (
|
({ video }: PlayListItem, index: number) => (
|
||||||
<div className={styles.videoWrapper}>
|
<div className={styles.videoWrapper}>
|
||||||
<video src={video} className={styles.video} loop muted playsInline />
|
<video
|
||||||
|
autoPlay={index === 0}
|
||||||
|
src={video}
|
||||||
|
className={styles.video}
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
[]
|
[]
|
||||||
@@ -117,6 +126,11 @@ export const AIOnboardingGeneral = ({
|
|||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// show dialog when it's mounted
|
||||||
|
useEffect(() => {
|
||||||
|
showAIOnboardingGeneral$.next(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const videoWrapper = videoWrapperRef.current;
|
const videoWrapper = videoWrapperRef.current;
|
||||||
if (!videoWrapper) return;
|
if (!videoWrapper) return;
|
||||||
@@ -136,7 +150,7 @@ export const AIOnboardingGeneral = ({
|
|||||||
return isCloud ? (
|
return isCloud ? (
|
||||||
<Modal
|
<Modal
|
||||||
open={open}
|
open={open}
|
||||||
onOpenChange={setOpen}
|
onOpenChange={v => showAIOnboardingGeneral$.next(v)}
|
||||||
contentOptions={{ className: styles.dialog }}
|
contentOptions={{ className: styles.dialog }}
|
||||||
overlayOptions={{ className: baseStyles.dialogOverlay }}
|
overlayOptions={{ className: baseStyles.dialogOverlay }}
|
||||||
>
|
>
|
||||||
|
|||||||
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
@@ -0,0 +1,8 @@
|
|||||||
|
import { LiveData } from '@toeverything/infra';
|
||||||
|
|
||||||
|
// to share the state between general & edgeless dialog,
|
||||||
|
// so that we can avoid showing edgeless dialog when general dialog is opened
|
||||||
|
export const showAIOnboardingGeneral$ = new LiveData(false);
|
||||||
|
|
||||||
|
// avoid notifying multiple times
|
||||||
|
export const edgelessNotifyId$ = new LiveData<string | number | null>(null);
|
||||||
@@ -1295,7 +1295,7 @@
|
|||||||
"com.affine.ai-onboarding.general.skip": "Alert me later",
|
"com.affine.ai-onboarding.general.skip": "Alert me later",
|
||||||
"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": "Try 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.title": "Meet AFFiNE AI",
|
||||||
"com.affine.ai-onboarding.edgeless.message": "Lets you think bigger, create faster, work smarter and save time for every project."
|
"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