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
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
9 changed files with 5649 additions and 45418 deletions

View File

@ -9,4 +9,13 @@ export const thumb = style({
height: 211,
background: cssVar('backgroundOverlayPanelColor'),
overflow: 'hidden',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
});
export const thumbContent = style({
borderRadius: 'inherit',
width: 'calc(100% + 4px)',
height: 'calc(100% + 4px)',
});

View File

@ -1,44 +1,48 @@
import { notify } from '@affine/component';
import { openSettingModalAtom } from '@affine/core/atoms';
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 { Doc, useLiveData, useService } from '@toeverything/infra';
import { cssVar } from '@toeverything/theme';
import { useAtomValue } from 'jotai';
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 mouseTrackDark from './lottie/edgeless/mouse-track-dark.json';
import mouseTrackLight from './lottie/edgeless/mouse-track-light.json';
import { edgelessNotifyId$, showAIOnboardingGeneral$ } from './state';
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 resolvedTheme === 'dark' ? mouseTrackDark : mouseTrackLight;
}, [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 = ({
onDismiss,
}: BaseAIOnboardingDialogProps) => {
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 currentWorkspace = useLiveData(
useService(CurrentWorkspaceService).currentWorkspace$
@ -49,6 +53,8 @@ export const AIOnboardingEdgeless = ({
const mode = useLiveData(doc.mode$);
useEffect(() => {
if (settingModalOpen.open) return;
if (generalAIOnboardingOpened) return;
if (notifyId) return;
if (isCloud && mode === 'edgeless') {
clearTimeout(timeoutRef.current);
@ -64,10 +70,18 @@ export const AIOnboardingEdgeless = ({
},
{ duration: 1000 * 60 * 10 }
);
notifyId$.next(id);
edgelessNotifyId$.next(id);
}, 1000);
}
}, [isCloud, mode, notifyId, onDismiss, t]);
}, [
generalAIOnboardingOpened,
isCloud,
mode,
notifyId,
onDismiss,
settingModalOpen,
t,
]);
return null;
};

View File

@ -13,6 +13,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import * as baseStyles from './base-style.css';
import * as styles from './general.dialog.css';
import { Slider } from './slider';
import { showAIOnboardingGeneral$ } from './state';
import type { BaseAIOnboardingDialogProps } from './type';
type PlayListItem = { video: string; title: ReactNode; desc: ReactNode };
@ -71,7 +72,8 @@ export const AIOnboardingGeneral = ({
);
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
const t = useAFFiNEI18N();
const [open, setOpen] = useState(true);
// const [open, setOpen] = useState(true);
const open = useLiveData(showAIOnboardingGeneral$);
const [index, setIndex] = useState(0);
const list = useMemo(() => getPlayList(t), [t]);
const setSettingModal = useSetAtom(openSettingModalAtom);
@ -81,7 +83,7 @@ export const AIOnboardingGeneral = ({
const isLast = index === list.length - 1;
const closeAndDismiss = useCallback(() => {
setOpen(false);
showAIOnboardingGeneral$.next(false);
onDismiss();
}, [onDismiss]);
const goToPricingPlans = useCallback(() => {
@ -92,7 +94,7 @@ export const AIOnboardingGeneral = ({
});
closeAndDismiss();
}, [closeAndDismiss, setSettingModal]);
const onClose = useCallback(() => setOpen(false), []);
const onClose = useCallback(() => showAIOnboardingGeneral$.next(false), []);
const onPrev = useCallback(() => {
setIndex(i => Math.max(0, i - 1));
}, []);
@ -101,9 +103,16 @@ export const AIOnboardingGeneral = ({
}, [list.length]);
const videoRenderer = useCallback(
({ video }: PlayListItem) => (
({ video }: PlayListItem, index: number) => (
<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>
),
[]
@ -117,6 +126,11 @@ export const AIOnboardingGeneral = ({
[]
);
// show dialog when it's mounted
useEffect(() => {
showAIOnboardingGeneral$.next(true);
}, []);
useEffect(() => {
const videoWrapper = videoWrapperRef.current;
if (!videoWrapper) return;
@ -136,7 +150,7 @@ export const AIOnboardingGeneral = ({
return isCloud ? (
<Modal
open={open}
onOpenChange={setOpen}
onOpenChange={v => showAIOnboardingGeneral$.next(v)}
contentOptions={{ className: styles.dialog }}
overlayOptions={{ className: baseStyles.dialogOverlay }}
>

View File

@ -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);

View File

@ -1295,7 +1295,7 @@
"com.affine.ai-onboarding.general.skip": "Alert me later",
"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.try-for-free": "Try for Free",
"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."