mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-25 10:22:55 +08:00
feat(core): expose toggle ai onboarding apis (#7039)
```ts
import { toggleGeneralAIOnboarding } from '@affine/core/components/affine/ai-onboarding/apis';
// show
// toggleGeneralAIOnboarding();
toggleGeneralAIOnboarding(true);
// dismiss
toggleGeneralAIOnboarding(false);
```
This commit is contained in:
@@ -0,0 +1,38 @@
|
|||||||
|
import { AIOnboardingType } from './type';
|
||||||
|
|
||||||
|
function createStorageEvent(key: string, newValue: string) {
|
||||||
|
const event = new StorageEvent('storage', {
|
||||||
|
key,
|
||||||
|
newValue,
|
||||||
|
oldValue: localStorage.getItem(key),
|
||||||
|
storageArea: localStorage,
|
||||||
|
});
|
||||||
|
window.dispatchEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
const setItem = function (key: string, value: string) {
|
||||||
|
const oldValue = localStorage.getItem(key);
|
||||||
|
localStorage.setItem.call(localStorage, key, value);
|
||||||
|
if (oldValue !== value) createStorageEvent(key, value);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show/Hide AI onboarding manually
|
||||||
|
*/
|
||||||
|
export const toggleGeneralAIOnboarding = (show = true) => {
|
||||||
|
setItem(AIOnboardingType.GENERAL, show ? 'false' : 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show/Hide local AI toast manually
|
||||||
|
*/
|
||||||
|
export const toggleLocalAIOnboarding = (show = true) => {
|
||||||
|
setItem(AIOnboardingType.LOCAL, show ? 'false' : 'true');
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show/Hide edgeless AI toast manually
|
||||||
|
*/
|
||||||
|
export const toggleEdgelessAIOnboarding = (show = true) => {
|
||||||
|
setItem(AIOnboardingType.EDGELESS, show ? 'false' : 'true');
|
||||||
|
};
|
||||||
@@ -2,7 +2,6 @@ import { Button, FlexWrapper, notify } from '@affine/component';
|
|||||||
import { openSettingModalAtom } from '@affine/core/atoms';
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
import { SubscriptionService } from '@affine/core/modules/cloud';
|
import { SubscriptionService } from '@affine/core/modules/cloud';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
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 {
|
import {
|
||||||
@@ -17,6 +16,7 @@ import Lottie from 'lottie-react';
|
|||||||
import { useTheme } from 'next-themes';
|
import { useTheme } from 'next-themes';
|
||||||
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
import { useCallback, useEffect, useMemo, useRef } from 'react';
|
||||||
|
|
||||||
|
import { toggleEdgelessAIOnboarding } from './apis';
|
||||||
import * as styles from './edgeless.dialog.css';
|
import * as styles from './edgeless.dialog.css';
|
||||||
import mouseTrackDark from './lottie/edgeless/mouse-track-dark.json';
|
import mouseTrackDark from './lottie/edgeless/mouse-track-dark.json';
|
||||||
import mouseTrackLight from './lottie/edgeless/mouse-track-light.json';
|
import mouseTrackLight from './lottie/edgeless/mouse-track-light.json';
|
||||||
@@ -25,7 +25,6 @@ import {
|
|||||||
localNotifyId$,
|
localNotifyId$,
|
||||||
showAIOnboardingGeneral$,
|
showAIOnboardingGeneral$,
|
||||||
} from './state';
|
} from './state';
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
|
||||||
|
|
||||||
const EdgelessOnboardingAnimation = () => {
|
const EdgelessOnboardingAnimation = () => {
|
||||||
const { resolvedTheme } = useTheme();
|
const { resolvedTheme } = useTheme();
|
||||||
@@ -46,10 +45,8 @@ const EdgelessOnboardingAnimation = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AIOnboardingEdgeless = ({
|
export const AIOnboardingEdgeless = () => {
|
||||||
onDismiss,
|
const { docService, subscriptionService } = useServices({
|
||||||
}: BaseAIOnboardingDialogProps) => {
|
|
||||||
const { workspaceService, docService, subscriptionService } = useServices({
|
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
DocService,
|
DocService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -61,8 +58,6 @@ export const AIOnboardingEdgeless = ({
|
|||||||
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
const aiSubscription = useLiveData(subscriptionService.subscription.ai$);
|
||||||
const settingModalOpen = useAtomValue(openSettingModalAtom);
|
const settingModalOpen = useAtomValue(openSettingModalAtom);
|
||||||
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const isCloud =
|
|
||||||
workspaceService.workspace.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
|
||||||
|
|
||||||
const setSettingModal = useSetAtom(openSettingModalAtom);
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
|
|
||||||
@@ -87,7 +82,6 @@ export const AIOnboardingEdgeless = ({
|
|||||||
if (generalAIOnboardingOpened) return;
|
if (generalAIOnboardingOpened) return;
|
||||||
if (notifyId) return;
|
if (notifyId) return;
|
||||||
if (mode !== 'edgeless') return;
|
if (mode !== 'edgeless') return;
|
||||||
if (!isCloud) return;
|
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
// try to close local onboarding
|
// try to close local onboarding
|
||||||
@@ -101,13 +95,13 @@ export const AIOnboardingEdgeless = ({
|
|||||||
iconColor: cssVar('processingColor'),
|
iconColor: cssVar('processingColor'),
|
||||||
thumb: <EdgelessOnboardingAnimation />,
|
thumb: <EdgelessOnboardingAnimation />,
|
||||||
alignMessage: 'icon',
|
alignMessage: 'icon',
|
||||||
onDismiss,
|
onDismiss: () => toggleEdgelessAIOnboarding(false),
|
||||||
footer: (
|
footer: (
|
||||||
<FlexWrapper marginTop={8} justifyContent="flex-end" gap="12px">
|
<FlexWrapper marginTop={8} justifyContent="flex-end" gap="12px">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
notify.dismiss(id);
|
notify.dismiss(id);
|
||||||
onDismiss();
|
toggleEdgelessAIOnboarding(false);
|
||||||
}}
|
}}
|
||||||
type="plain"
|
type="plain"
|
||||||
className={styles.actionButton}
|
className={styles.actionButton}
|
||||||
@@ -123,7 +117,7 @@ export const AIOnboardingEdgeless = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
goToPricingPlans();
|
goToPricingPlans();
|
||||||
notify.dismiss(id);
|
notify.dismiss(id);
|
||||||
onDismiss();
|
toggleEdgelessAIOnboarding(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className={styles.purchaseButtonText}>
|
<span className={styles.purchaseButtonText}>
|
||||||
@@ -142,10 +136,8 @@ export const AIOnboardingEdgeless = ({
|
|||||||
aiSubscription,
|
aiSubscription,
|
||||||
generalAIOnboardingOpened,
|
generalAIOnboardingOpened,
|
||||||
goToPricingPlans,
|
goToPricingPlans,
|
||||||
isCloud,
|
|
||||||
mode,
|
mode,
|
||||||
notifyId,
|
notifyId,
|
||||||
onDismiss,
|
|
||||||
settingModalOpen,
|
settingModalOpen,
|
||||||
t,
|
t,
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ import { useAtom } from 'jotai';
|
|||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||||
|
|
||||||
|
import { toggleGeneralAIOnboarding } from './apis';
|
||||||
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 { showAIOnboardingGeneral$ } from './state';
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
|
||||||
|
|
||||||
type PlayListItem = { video: string; title: ReactNode; desc: ReactNode };
|
type PlayListItem = { video: string; title: ReactNode; desc: ReactNode };
|
||||||
type Translate = ReturnType<typeof useAFFiNEI18N>;
|
type Translate = ReturnType<typeof useAFFiNEI18N>;
|
||||||
@@ -82,9 +82,7 @@ function prefetchVideos() {
|
|||||||
prefetched = true;
|
prefetched = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AIOnboardingGeneral = ({
|
export const AIOnboardingGeneral = () => {
|
||||||
onDismiss,
|
|
||||||
}: BaseAIOnboardingDialogProps) => {
|
|
||||||
const { authService, subscriptionService } = useServices({
|
const { authService, subscriptionService } = useServices({
|
||||||
AuthService,
|
AuthService,
|
||||||
SubscriptionService,
|
SubscriptionService,
|
||||||
@@ -111,8 +109,8 @@ export const AIOnboardingGeneral = ({
|
|||||||
}, []);
|
}, []);
|
||||||
const closeAndDismiss = useCallback(() => {
|
const closeAndDismiss = useCallback(() => {
|
||||||
showAIOnboardingGeneral$.next(false);
|
showAIOnboardingGeneral$.next(false);
|
||||||
onDismiss();
|
toggleGeneralAIOnboarding(false);
|
||||||
}, [onDismiss]);
|
}, []);
|
||||||
const goToPricingPlans = useCallback(() => {
|
const goToPricingPlans = useCallback(() => {
|
||||||
setSettingModal({
|
setSettingModal({
|
||||||
open: true,
|
open: true,
|
||||||
@@ -190,7 +188,7 @@ export const AIOnboardingGeneral = ({
|
|||||||
open={open}
|
open={open}
|
||||||
onOpenChange={v => {
|
onOpenChange={v => {
|
||||||
showAIOnboardingGeneral$.next(v);
|
showAIOnboardingGeneral$.next(v);
|
||||||
if (!v && isLast) onDismiss();
|
if (!v && isLast) toggleGeneralAIOnboarding(false);
|
||||||
}}
|
}}
|
||||||
contentOptions={{ className: styles.dialog }}
|
contentOptions={{ className: styles.dialog }}
|
||||||
overlayOptions={{ className: baseStyles.dialogOverlay }}
|
overlayOptions={{ className: baseStyles.dialogOverlay }}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { Suspense, useCallback, useState } from 'react';
|
import { Suspense, useCallback, useEffect, useState } from 'react';
|
||||||
|
|
||||||
import { AIOnboardingEdgeless } from './edgeless.dialog';
|
import { AIOnboardingEdgeless } from './edgeless.dialog';
|
||||||
import { AIOnboardingGeneral } from './general.dialog';
|
import { AIOnboardingGeneral } from './general.dialog';
|
||||||
@@ -8,6 +8,15 @@ import { AIOnboardingType } from './type';
|
|||||||
const useDismiss = (key: AIOnboardingType) => {
|
const useDismiss = (key: AIOnboardingType) => {
|
||||||
const [dismiss, setDismiss] = useState(localStorage.getItem(key) === 'true');
|
const [dismiss, setDismiss] = useState(localStorage.getItem(key) === 'true');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handler = (e: StorageEvent) => {
|
||||||
|
if (e.key !== key) return;
|
||||||
|
setDismiss(localStorage.getItem(key) === 'true');
|
||||||
|
};
|
||||||
|
window.addEventListener('storage', handler);
|
||||||
|
return () => window.removeEventListener('storage', handler);
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
const onDismiss = useCallback(() => {
|
const onDismiss = useCallback(() => {
|
||||||
setDismiss(true);
|
setDismiss(true);
|
||||||
localStorage.setItem(key, 'true');
|
localStorage.setItem(key, 'true');
|
||||||
@@ -17,32 +26,21 @@ const useDismiss = (key: AIOnboardingType) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const WorkspaceAIOnboarding = () => {
|
export const WorkspaceAIOnboarding = () => {
|
||||||
const [dismissGeneral, onDismissGeneral] = useDismiss(
|
const [dismissGeneral] = useDismiss(AIOnboardingType.GENERAL);
|
||||||
AIOnboardingType.GENERAL
|
const [dismissLocal] = useDismiss(AIOnboardingType.LOCAL);
|
||||||
);
|
|
||||||
const [dismissLocal, onDismissLocal] = useDismiss(AIOnboardingType.LOCAL);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>
|
||||||
{dismissGeneral ? null : (
|
{dismissGeneral ? null : <AIOnboardingGeneral />}
|
||||||
<AIOnboardingGeneral onDismiss={onDismissGeneral} />
|
{dismissLocal ? null : <AIOnboardingLocal />}
|
||||||
)}
|
|
||||||
|
|
||||||
{dismissLocal ? null : <AIOnboardingLocal onDismiss={onDismissLocal} />}
|
|
||||||
</Suspense>
|
</Suspense>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const PageAIOnboarding = () => {
|
export const PageAIOnboarding = () => {
|
||||||
const [dismissEdgeless, onDismissEdgeless] = useDismiss(
|
const [dismissEdgeless] = useDismiss(AIOnboardingType.EDGELESS);
|
||||||
AIOnboardingType.EDGELESS
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Suspense>
|
<Suspense>{dismissEdgeless ? null : <AIOnboardingEdgeless />}</Suspense>
|
||||||
{dismissEdgeless ? null : (
|
|
||||||
<AIOnboardingEdgeless onDismiss={onDismissEdgeless} />
|
|
||||||
)}
|
|
||||||
</Suspense>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -10,9 +10,9 @@ import { useLiveData, useService } from '@toeverything/infra';
|
|||||||
import { cssVar } from '@toeverything/theme';
|
import { cssVar } from '@toeverything/theme';
|
||||||
import { useEffect, useRef } from 'react';
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import { toggleLocalAIOnboarding } from './apis';
|
||||||
import * as styles from './local.dialog.css';
|
import * as styles from './local.dialog.css';
|
||||||
import { edgelessNotifyId$, localNotifyId$ } from './state';
|
import { edgelessNotifyId$, localNotifyId$ } from './state';
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
|
||||||
|
|
||||||
const LocalOnboardingAnimation = () => {
|
const LocalOnboardingAnimation = () => {
|
||||||
return (
|
return (
|
||||||
@@ -63,9 +63,7 @@ const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AIOnboardingLocal = ({
|
export const AIOnboardingLocal = () => {
|
||||||
onDismiss,
|
|
||||||
}: BaseAIOnboardingDialogProps) => {
|
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
const authService = useService(AuthService);
|
const authService = useService(AuthService);
|
||||||
const notifyId = useLiveData(localNotifyId$);
|
const notifyId = useLiveData(localNotifyId$);
|
||||||
@@ -94,11 +92,11 @@ export const AIOnboardingLocal = ({
|
|||||||
iconColor: cssVar('brandColor'),
|
iconColor: cssVar('brandColor'),
|
||||||
thumb: <LocalOnboardingAnimation />,
|
thumb: <LocalOnboardingAnimation />,
|
||||||
alignMessage: 'icon',
|
alignMessage: 'icon',
|
||||||
onDismiss,
|
onDismiss: () => toggleLocalAIOnboarding(false),
|
||||||
footer: (
|
footer: (
|
||||||
<FooterActions
|
<FooterActions
|
||||||
onDismiss={() => {
|
onDismiss={() => {
|
||||||
onDismiss();
|
toggleLocalAIOnboarding(false);
|
||||||
notify.dismiss(id);
|
notify.dismiss(id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -109,7 +107,7 @@ export const AIOnboardingLocal = ({
|
|||||||
);
|
);
|
||||||
localNotifyId$.next(id);
|
localNotifyId$.next(id);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}, [notSignedIn, notifyId, onDismiss, t]);
|
}, [notSignedIn, notifyId, t]);
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,6 +1,3 @@
|
|||||||
export interface BaseAIOnboardingDialogProps {
|
|
||||||
onDismiss: () => void;
|
|
||||||
}
|
|
||||||
export enum AIOnboardingType {
|
export enum AIOnboardingType {
|
||||||
GENERAL = 'dismissAiOnboarding',
|
GENERAL = 'dismissAiOnboarding',
|
||||||
EDGELESS = 'dismissAiOnboardingEdgeless',
|
EDGELESS = 'dismissAiOnboardingEdgeless',
|
||||||
|
|||||||
Reference in New Issue
Block a user