feat(core): add local ai onboarding dialog (#6600)

This commit is contained in:
CatsJuice 2024-04-18 15:48:19 +00:00
parent 28f2ff24b9
commit 7970d9b8c9
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
6 changed files with 172 additions and 24 deletions

View File

@ -30,6 +30,7 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => {
footer,
alignMessage = 'title',
onDismiss,
rootAttrs,
} = notification;
const onActionClicked = useCallback(() => {
@ -49,7 +50,8 @@ export const NotificationCard = ({ notification }: NotificationCardProps) => {
[styles.iconColor]: getIconColor(style, theme, iconColor),
})}
data-with-icon={icon ? '' : undefined}
className={styles.card}
{...rootAttrs}
className={clsx(styles.card, rootAttrs?.className)}
>
{thumb}
<div className={styles.cardInner}>

View File

@ -1,4 +1,4 @@
import type { ReactNode } from 'react';
import type { HTMLAttributes, ReactNode } from 'react';
import type { ButtonProps } from '../button';
@ -23,6 +23,8 @@ export interface Notification {
autoClose?: boolean;
};
rootAttrs?: HTMLAttributes<HTMLDivElement>;
// custom slots
thumb?: ReactNode;
title?: ReactNode;

View File

@ -18,7 +18,11 @@ import { useEffect, useMemo, useRef } from 'react';
import * as styles from './edgeless.dialog.css';
import mouseTrackDark from './lottie/edgeless/mouse-track-dark.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';
const EdgelessOnboardingAnimation = () => {
@ -63,24 +67,27 @@ export const AIOnboardingEdgeless = ({
if (settingModalOpen.open) return;
if (generalAIOnboardingOpened) return;
if (notifyId) return;
if (isCloud && mode === 'edgeless') {
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
const id = notify(
{
title: t['com.affine.ai-onboarding.edgeless.title'](),
message: t['com.affine.ai-onboarding.edgeless.message'](),
icon: <AiIcon />,
iconColor: cssVar('brandColor'),
thumb: <EdgelessOnboardingAnimation />,
alignMessage: 'icon',
onDismiss,
},
{ duration: 1000 * 60 * 10 }
);
edgelessNotifyId$.next(id);
}, 1000);
}
if (mode !== 'edgeless') return;
if (!isCloud) return;
clearTimeout(timeoutRef.current);
timeoutRef.current = setTimeout(() => {
// try to close local onboarding
notify.dismiss(localNotifyId$.value);
const id = notify(
{
title: t['com.affine.ai-onboarding.edgeless.title'](),
message: t['com.affine.ai-onboarding.edgeless.message'](),
icon: <AiIcon />,
iconColor: cssVar('processingColor'),
thumb: <EdgelessOnboardingAnimation />,
alignMessage: 'icon',
onDismiss,
},
{ duration: 1000 * 60 * 10 }
);
edgelessNotifyId$.next(id);
}, 1000);
}, [
generalAIOnboardingOpened,
isCloud,

View File

@ -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',
});

View File

@ -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';
export const AIOnboardingLocal = (_: BaseAIOnboardingDialogProps) => {
return <div>{/* TODO: open local workspace for the first time */}</div>;
const LocalOnboardingAnimation = () => {
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;
};

View File

@ -5,4 +5,10 @@ import { LiveData } from '@toeverything/infra';
export const showAIOnboardingGeneral$ = new LiveData(false);
// 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
);