mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-08-22 12:32:00 +08:00
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 }}
|
||||||
>
|
>
|
||||||
|
|||||||
+2778
-704
File diff suppressed because it is too large
Load Diff
+2800
-704
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
@@ -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