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:
CatsJuice
2024-04-16 13:18:09 +00:00
parent c222cf7b96
commit bb329944ed
9 changed files with 5649 additions and 45418 deletions
@@ -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 }}
> >
@@ -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);
+1 -1
View File
@@ -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."