feat(core): add local ai onboarding dialog (#6600)
This commit is contained in:
@@ -30,6 +30,7 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
|||||||
footer,
|
footer,
|
||||||
alignMessage = 'title',
|
alignMessage = 'title',
|
||||||
onDismiss,
|
onDismiss,
|
||||||
|
rootAttrs,
|
||||||
} = notification;
|
} = notification;
|
||||||
|
|
||||||
const onActionClicked = useCallback(() => {
|
const onActionClicked = useCallback(() => {
|
||||||
@@ -49,7 +50,8 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => {
|
|||||||
[styles.iconColor]: getIconColor(style, theme, iconColor),
|
[styles.iconColor]: getIconColor(style, theme, iconColor),
|
||||||
})}
|
})}
|
||||||
data-with-icon={icon ? '' : undefined}
|
data-with-icon={icon ? '' : undefined}
|
||||||
className={styles.card}
|
{...rootAttrs}
|
||||||
|
className={clsx(styles.card, rootAttrs?.className)}
|
||||||
>
|
>
|
||||||
{thumb}
|
{thumb}
|
||||||
<div className={styles.cardInner}>
|
<div className={styles.cardInner}>
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { ReactNode } from 'react';
|
import type { HTMLAttributes, ReactNode } from 'react';
|
||||||
|
|
||||||
import type { ButtonProps } from '../button';
|
import type { ButtonProps } from '../button';
|
||||||
|
|
||||||
@@ -23,6 +23,8 @@ export interface Notification {
|
|||||||
autoClose?: boolean;
|
autoClose?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
rootAttrs?: HTMLAttributes<HTMLDivElement>;
|
||||||
|
|
||||||
// custom slots
|
// custom slots
|
||||||
thumb?: ReactNode;
|
thumb?: ReactNode;
|
||||||
title?: ReactNode;
|
title?: ReactNode;
|
||||||
|
|||||||
@@ -18,7 +18,11 @@ import { useEffect, useMemo, useRef } from 'react';
|
|||||||
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';
|
||||||
import { edgelessNotifyId$, showAIOnboardingGeneral$ } from './state';
|
import {
|
||||||
|
edgelessNotifyId$,
|
||||||
|
localNotifyId$,
|
||||||
|
showAIOnboardingGeneral$,
|
||||||
|
} from './state';
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
import type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
const EdgelessOnboardingAnimation = () => {
|
const EdgelessOnboardingAnimation = () => {
|
||||||
@@ -63,15 +67,19 @@ export const AIOnboardingEdgeless = ({
|
|||||||
if (settingModalOpen.open) return;
|
if (settingModalOpen.open) return;
|
||||||
if (generalAIOnboardingOpened) return;
|
if (generalAIOnboardingOpened) return;
|
||||||
if (notifyId) return;
|
if (notifyId) return;
|
||||||
if (isCloud && mode === 'edgeless') {
|
if (mode !== 'edgeless') return;
|
||||||
|
if (!isCloud) return;
|
||||||
clearTimeout(timeoutRef.current);
|
clearTimeout(timeoutRef.current);
|
||||||
timeoutRef.current = setTimeout(() => {
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
// try to close local onboarding
|
||||||
|
notify.dismiss(localNotifyId$.value);
|
||||||
|
|
||||||
const id = notify(
|
const id = notify(
|
||||||
{
|
{
|
||||||
title: t['com.affine.ai-onboarding.edgeless.title'](),
|
title: t['com.affine.ai-onboarding.edgeless.title'](),
|
||||||
message: t['com.affine.ai-onboarding.edgeless.message'](),
|
message: t['com.affine.ai-onboarding.edgeless.message'](),
|
||||||
icon: <AiIcon />,
|
icon: <AiIcon />,
|
||||||
iconColor: cssVar('brandColor'),
|
iconColor: cssVar('processingColor'),
|
||||||
thumb: <EdgelessOnboardingAnimation />,
|
thumb: <EdgelessOnboardingAnimation />,
|
||||||
alignMessage: 'icon',
|
alignMessage: 'icon',
|
||||||
onDismiss,
|
onDismiss,
|
||||||
@@ -80,7 +88,6 @@ export const AIOnboardingEdgeless = ({
|
|||||||
);
|
);
|
||||||
edgelessNotifyId$.next(id);
|
edgelessNotifyId$.next(id);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
|
||||||
}, [
|
}, [
|
||||||
generalAIOnboardingOpened,
|
generalAIOnboardingOpened,
|
||||||
isCloud,
|
isCloud,
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const card = style({
|
||||||
|
borderRadius: 12,
|
||||||
|
boxShadow: cssVar('menuShadow'),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const thumb = style({
|
||||||
|
width: '100%',
|
||||||
|
height: 211,
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
overflow: 'hidden',
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'center',
|
||||||
|
alignItems: 'center',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const thumbContent = style({
|
||||||
|
width: 'calc(100% + 4px)',
|
||||||
|
height: 'calc(100% + 4px)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
fontWeight: 500,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const footerActions = style({
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
gap: 12,
|
||||||
|
marginTop: 8,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const actionButton = style({
|
||||||
|
fontSize: cssVar('fontSm'),
|
||||||
|
padding: '0 2px',
|
||||||
|
});
|
||||||
@@ -1,5 +1,96 @@
|
|||||||
|
import { Button, notify } from '@affine/component';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { AiIcon } from '@blocksuite/icons';
|
||||||
|
import { useLiveData, useService, WorkspaceService } from '@toeverything/infra';
|
||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './local.dialog.css';
|
||||||
|
import { edgelessNotifyId$, localNotifyId$ } from './state';
|
||||||
import type { BaseAIOnboardingDialogProps } from './type';
|
import type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
export const AIOnboardingLocal = (_: BaseAIOnboardingDialogProps) => {
|
const LocalOnboardingAnimation = () => {
|
||||||
return <div>{/* TODO: open local workspace for the first time */}</div>;
|
return (
|
||||||
|
<div className={styles.thumb}>
|
||||||
|
<video
|
||||||
|
className={styles.thumbContent}
|
||||||
|
src="/onboarding/ai-onboarding.general.1.mov"
|
||||||
|
autoPlay
|
||||||
|
loop
|
||||||
|
muted
|
||||||
|
playsInline
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const FooterActions = ({ onDismiss }: { onDismiss: () => void }) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
return (
|
||||||
|
<div className={styles.footerActions}>
|
||||||
|
<Button onClick={onDismiss} type="plain" className={styles.actionButton}>
|
||||||
|
<span style={{ color: cssVar('textSecondaryColor') }}>
|
||||||
|
{t['com.affine.ai-onboarding.local.action-dismiss']()}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
<a href="https://ai.affine.pro" target="_blank" rel="noreferrer">
|
||||||
|
<Button className={styles.actionButton} type="plain">
|
||||||
|
<span style={{ color: cssVar('textPrimaryColor') }}>
|
||||||
|
{t['com.affine.ai-onboarding.local.action-learn-more']()}
|
||||||
|
</span>
|
||||||
|
</Button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AIOnboardingLocal = ({
|
||||||
|
onDismiss,
|
||||||
|
}: BaseAIOnboardingDialogProps) => {
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const workspaceService = useService(WorkspaceService);
|
||||||
|
const notifyId = useLiveData(localNotifyId$);
|
||||||
|
const timeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
const isLocal = workspaceService.workspace.flavour === WorkspaceFlavour.LOCAL;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isLocal) return;
|
||||||
|
if (notifyId) return;
|
||||||
|
clearTimeout(timeoutRef.current);
|
||||||
|
timeoutRef.current = setTimeout(() => {
|
||||||
|
// try to close edgeless onboarding
|
||||||
|
notify.dismiss(edgelessNotifyId$.value);
|
||||||
|
|
||||||
|
const id = notify(
|
||||||
|
{
|
||||||
|
title: (
|
||||||
|
<div className={styles.title}>
|
||||||
|
{t['com.affine.ai-onboarding.local.title']()}
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
message: t['com.affine.ai-onboarding.local.message'](),
|
||||||
|
icon: <AiIcon />,
|
||||||
|
iconColor: cssVar('brandColor'),
|
||||||
|
thumb: <LocalOnboardingAnimation />,
|
||||||
|
alignMessage: 'icon',
|
||||||
|
onDismiss,
|
||||||
|
footer: (
|
||||||
|
<FooterActions
|
||||||
|
onDismiss={() => {
|
||||||
|
onDismiss();
|
||||||
|
notify.dismiss(id);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
rootAttrs: { className: styles.card },
|
||||||
|
},
|
||||||
|
{ duration: 1000 * 60 * 10 }
|
||||||
|
);
|
||||||
|
localNotifyId$.next(id);
|
||||||
|
}, 1000);
|
||||||
|
}, [isLocal, notifyId, onDismiss, t]);
|
||||||
|
|
||||||
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,4 +5,10 @@ import { LiveData } from '@toeverything/infra';
|
|||||||
export const showAIOnboardingGeneral$ = new LiveData(false);
|
export const showAIOnboardingGeneral$ = new LiveData(false);
|
||||||
|
|
||||||
// avoid notifying multiple times
|
// avoid notifying multiple times
|
||||||
export const edgelessNotifyId$ = new LiveData<string | number | null>(null);
|
export const edgelessNotifyId$ = new LiveData<string | number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
export const localNotifyId$ = new LiveData<string | number | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user