feat(core): add ai onboarding (#6544)
This commit is contained in:
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,29 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const dialogOverlay = style({
|
||||||
|
background: `linear-gradient(95deg, transparent 0px, ${cssVar('backgroundPrimaryColor')} 400px)`,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideTransition = style({
|
||||||
|
transition: 'all 0.3s',
|
||||||
|
|
||||||
|
selectors: {
|
||||||
|
'&.preEnter, &.exiting': {
|
||||||
|
opacity: 0,
|
||||||
|
position: 'absolute',
|
||||||
|
},
|
||||||
|
'&.preEnter.left, &.exiting.left': {
|
||||||
|
transform: 'translateX(-100%)',
|
||||||
|
},
|
||||||
|
'&.preEnter.right, &.exiting.right': {
|
||||||
|
transform: 'translateX(100%)',
|
||||||
|
},
|
||||||
|
'&.exited:not([data-force-render="true"])': {
|
||||||
|
display: 'none',
|
||||||
|
},
|
||||||
|
'&.exited[data-force-render="true"]': {
|
||||||
|
visibility: 'hidden',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
import type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
|
export const AIOnboardingEdgeless = ({
|
||||||
|
onDismiss: _,
|
||||||
|
}: BaseAIOnboardingDialogProps) => {
|
||||||
|
return (
|
||||||
|
<div>{/* TODO: open edgeless in cloud workspace for the first time */}</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
import { cssVar } from '@toeverything/theme';
|
||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const dialog = style({
|
||||||
|
maxWidth: 400,
|
||||||
|
width: 'calc(100% - 32px)',
|
||||||
|
padding: 0,
|
||||||
|
boxShadow: 'none',
|
||||||
|
'::after': {
|
||||||
|
content: '""',
|
||||||
|
position: 'absolute',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
boxShadow: cssVar('menuShadow'),
|
||||||
|
pointerEvents: 'none',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
export const dialogContent = style({
|
||||||
|
overflow: 'hidden',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const videoHeader = style({
|
||||||
|
borderRadius: 'inherit',
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderBottomRightRadius: 0,
|
||||||
|
width: '100%',
|
||||||
|
height: 225,
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
export const videoWrapper = style({
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
position: 'relative',
|
||||||
|
overflow: 'hidden',
|
||||||
|
});
|
||||||
|
export const video = style({
|
||||||
|
position: 'absolute',
|
||||||
|
left: -2,
|
||||||
|
top: -2,
|
||||||
|
width: 'calc(100% + 4px)',
|
||||||
|
height: 'calc(100% + 4px)',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const title = style({
|
||||||
|
padding: '20px 24px 8px 24px',
|
||||||
|
fontSize: cssVar('fontH6'),
|
||||||
|
fontWeight: 600,
|
||||||
|
lineHeight: '26px',
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
});
|
||||||
|
export const description = style({
|
||||||
|
padding: '0px 24px',
|
||||||
|
fontSize: cssVar('fontBase'),
|
||||||
|
lineHeight: '24px',
|
||||||
|
minHeight: 48,
|
||||||
|
fontWeight: 400,
|
||||||
|
color: cssVar('textPrimaryColor'),
|
||||||
|
});
|
||||||
|
export const link = style({
|
||||||
|
color: cssVar('textEmphasisColor'),
|
||||||
|
textDecoration: 'underline',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const footer = style({
|
||||||
|
padding: '20px 28px',
|
||||||
|
gap: 12,
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'flex-end',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const skipButton = style({
|
||||||
|
fontWeight: 500,
|
||||||
|
});
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
import { Button, Modal } from '@affine/component';
|
||||||
|
import { openSettingModalAtom } from '@affine/core/atoms';
|
||||||
|
import { useBlurRoot } from '@affine/core/hooks/use-blur-root';
|
||||||
|
import { CurrentWorkspaceService } from '@affine/core/modules/workspace';
|
||||||
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
|
import { Trans } from '@affine/i18n';
|
||||||
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useSetAtom } from 'jotai';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
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 type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
|
type PlayListItem = { video: string; title: ReactNode; desc: ReactNode };
|
||||||
|
type Translate = ReturnType<typeof useAFFiNEI18N>;
|
||||||
|
|
||||||
|
const getPlayList = (t: Translate): Array<PlayListItem> => [
|
||||||
|
{
|
||||||
|
video: '/onboarding/ai-onboarding.general.1.mov',
|
||||||
|
title: t['com.affine.ai-onboarding.general.1.title'](),
|
||||||
|
desc: t['com.affine.ai-onboarding.general.1.description'](),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
video: '/onboarding/ai-onboarding.general.2.mov',
|
||||||
|
title: t['com.affine.ai-onboarding.general.2.title'](),
|
||||||
|
desc: t['com.affine.ai-onboarding.general.2.description'](),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
video: '/onboarding/ai-onboarding.general.3.mov',
|
||||||
|
title: t['com.affine.ai-onboarding.general.3.title'](),
|
||||||
|
desc: t['com.affine.ai-onboarding.general.3.description'](),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
video: '/onboarding/ai-onboarding.general.4.mov',
|
||||||
|
title: t['com.affine.ai-onboarding.general.4.title'](),
|
||||||
|
desc: t['com.affine.ai-onboarding.general.4.description'](),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
video: '/onboarding/ai-onboarding.general.1.mov',
|
||||||
|
title: t['com.affine.ai-onboarding.general.5.title'](),
|
||||||
|
desc: (
|
||||||
|
<Trans
|
||||||
|
i18nKey="com.affine.ai-onboarding.general.5.description"
|
||||||
|
values={{ link: 'ai.affine.pro' }}
|
||||||
|
components={{
|
||||||
|
a: (
|
||||||
|
<a
|
||||||
|
className={styles.link}
|
||||||
|
href="https://ai.affine.pro"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const AIOnboardingGeneral = ({
|
||||||
|
onDismiss,
|
||||||
|
}: BaseAIOnboardingDialogProps) => {
|
||||||
|
const videoWrapperRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
const prevVideoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
const currentWorkspace = useLiveData(
|
||||||
|
useService(CurrentWorkspaceService).currentWorkspace$
|
||||||
|
);
|
||||||
|
const isCloud = currentWorkspace?.flavour === WorkspaceFlavour.AFFINE_CLOUD;
|
||||||
|
const t = useAFFiNEI18N();
|
||||||
|
const [open, setOpen] = useState(true);
|
||||||
|
const [index, setIndex] = useState(0);
|
||||||
|
const list = useMemo(() => getPlayList(t), [t]);
|
||||||
|
const setSettingModal = useSetAtom(openSettingModalAtom);
|
||||||
|
useBlurRoot(open && isCloud);
|
||||||
|
|
||||||
|
const isFirst = index === 0;
|
||||||
|
const isLast = index === list.length - 1;
|
||||||
|
|
||||||
|
const closeAndDismiss = useCallback(() => {
|
||||||
|
setOpen(false);
|
||||||
|
onDismiss();
|
||||||
|
}, [onDismiss]);
|
||||||
|
const goToPricingPlans = useCallback(() => {
|
||||||
|
setSettingModal({
|
||||||
|
open: true,
|
||||||
|
activeTab: 'plans',
|
||||||
|
scrollAnchor: 'aiPricingPlan',
|
||||||
|
});
|
||||||
|
closeAndDismiss();
|
||||||
|
}, [closeAndDismiss, setSettingModal]);
|
||||||
|
const onClose = useCallback(() => setOpen(false), []);
|
||||||
|
const onPrev = useCallback(() => {
|
||||||
|
setIndex(i => Math.max(0, i - 1));
|
||||||
|
}, []);
|
||||||
|
const onNext = useCallback(() => {
|
||||||
|
setIndex(i => Math.min(list.length - 1, i + 1));
|
||||||
|
}, [list.length]);
|
||||||
|
|
||||||
|
const videoRenderer = useCallback(
|
||||||
|
({ video }: PlayListItem) => (
|
||||||
|
<div className={styles.videoWrapper}>
|
||||||
|
<video src={video} className={styles.video} loop muted playsInline />
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const titleRenderer = useCallback(
|
||||||
|
({ title }: PlayListItem) => <h1 className={styles.title}>{title}</h1>,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
const descriptionRenderer = useCallback(
|
||||||
|
({ desc }: PlayListItem) => <p className={styles.description}>{desc}</p>,
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const videoWrapper = videoWrapperRef.current;
|
||||||
|
if (!videoWrapper) return;
|
||||||
|
|
||||||
|
const videos = videoWrapper.querySelectorAll('video');
|
||||||
|
const video = videos[index];
|
||||||
|
if (!video) return;
|
||||||
|
|
||||||
|
if (prevVideoRef.current) {
|
||||||
|
prevVideoRef.current.pause();
|
||||||
|
}
|
||||||
|
|
||||||
|
video.play().catch(console.error);
|
||||||
|
prevVideoRef.current = video;
|
||||||
|
}, [index]);
|
||||||
|
|
||||||
|
return isCloud ? (
|
||||||
|
<Modal
|
||||||
|
open={open}
|
||||||
|
onOpenChange={setOpen}
|
||||||
|
contentOptions={{ className: styles.dialog }}
|
||||||
|
overlayOptions={{ className: baseStyles.dialogOverlay }}
|
||||||
|
>
|
||||||
|
<div className={styles.dialogContent}>
|
||||||
|
<Slider<PlayListItem>
|
||||||
|
rootRef={videoWrapperRef}
|
||||||
|
className={styles.videoHeader}
|
||||||
|
items={list}
|
||||||
|
activeIndex={index}
|
||||||
|
preload={5}
|
||||||
|
itemRenderer={videoRenderer}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<Slider<PlayListItem>
|
||||||
|
items={list}
|
||||||
|
activeIndex={index}
|
||||||
|
itemRenderer={titleRenderer}
|
||||||
|
transitionDuration={400}
|
||||||
|
/>
|
||||||
|
<Slider<PlayListItem>
|
||||||
|
items={list}
|
||||||
|
activeIndex={index}
|
||||||
|
itemRenderer={descriptionRenderer}
|
||||||
|
transitionDuration={500}
|
||||||
|
/>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer className={styles.footer}>
|
||||||
|
{isLast ? (
|
||||||
|
<>
|
||||||
|
<Button onClick={closeAndDismiss}>
|
||||||
|
{t['com.affine.ai-onboarding.general.try-for-free']()}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={goToPricingPlans} type="primary">
|
||||||
|
{t['com.affine.ai-onboarding.general.purchase']()}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
{isFirst ? (
|
||||||
|
<Button onClick={onClose} className={styles.skipButton}>
|
||||||
|
{t['com.affine.ai-onboarding.general.skip']()}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button onClick={onPrev}>
|
||||||
|
{t['com.affine.ai-onboarding.general.prev']()}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button type="primary" onClick={onNext}>
|
||||||
|
{t['com.affine.ai-onboarding.general.next']()}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
) : null;
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { Suspense, useCallback, useState } from 'react';
|
||||||
|
|
||||||
|
import { AIOnboardingEdgeless } from './edgeless.dialog';
|
||||||
|
import { AIOnboardingGeneral } from './general.dialog';
|
||||||
|
import { AIOnboardingLocal } from './local.dialog';
|
||||||
|
import { AIOnboardingType } from './type';
|
||||||
|
|
||||||
|
const useDismiss = (key: AIOnboardingType) => {
|
||||||
|
const [dismiss, setDismiss] = useState(localStorage.getItem(key) === 'true');
|
||||||
|
|
||||||
|
const onDismiss = useCallback(() => {
|
||||||
|
setDismiss(true);
|
||||||
|
localStorage.setItem(key, 'true');
|
||||||
|
}, [key]);
|
||||||
|
|
||||||
|
return [dismiss, onDismiss] as const;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const AIOnboarding = () => {
|
||||||
|
const [dismissGeneral, onDismissGeneral] = useDismiss(
|
||||||
|
AIOnboardingType.GENERAL
|
||||||
|
);
|
||||||
|
const [dismissEdgeless, onDismissEdgeless] = useDismiss(
|
||||||
|
AIOnboardingType.EDGELESS
|
||||||
|
);
|
||||||
|
const [dismissLocal, onDismissLocal] = useDismiss(AIOnboardingType.LOCAL);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Suspense>
|
||||||
|
{dismissGeneral ? null : (
|
||||||
|
<AIOnboardingGeneral onDismiss={onDismissGeneral} />
|
||||||
|
)}
|
||||||
|
{dismissEdgeless ? null : (
|
||||||
|
<AIOnboardingEdgeless onDismiss={onDismissEdgeless} />
|
||||||
|
)}
|
||||||
|
{dismissLocal ? null : <AIOnboardingLocal onDismiss={onDismissLocal} />}
|
||||||
|
</Suspense>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import type { BaseAIOnboardingDialogProps } from './type';
|
||||||
|
|
||||||
|
export const AIOnboardingLocal = (_: BaseAIOnboardingDialogProps) => {
|
||||||
|
return <div>{/* TODO: open local workspace for the first time */}</div>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
|
export const slider = style({
|
||||||
|
overflow: 'clip',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const sliderContent = style({
|
||||||
|
display: 'flex',
|
||||||
|
height: '100%',
|
||||||
|
willChange: 'transform',
|
||||||
|
});
|
||||||
|
|
||||||
|
export const slideItem = style({
|
||||||
|
width: 0,
|
||||||
|
flex: 1,
|
||||||
|
});
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import clsx from 'clsx';
|
||||||
|
import { type HTMLAttributes, type Ref } from 'react';
|
||||||
|
|
||||||
|
import * as styles from './slider.css';
|
||||||
|
|
||||||
|
export interface SliderProps<T> extends HTMLAttributes<HTMLDivElement> {
|
||||||
|
items: T[];
|
||||||
|
activeIndex?: number;
|
||||||
|
itemRenderer?: (item: T, index: number) => React.ReactNode;
|
||||||
|
/**
|
||||||
|
* preload next and previous slides
|
||||||
|
*/
|
||||||
|
preload?: number;
|
||||||
|
transitionDuration?: number;
|
||||||
|
transitionTimingFunction?: string;
|
||||||
|
|
||||||
|
rootRef?: Ref<HTMLDivElement>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* TODO: extract to @affine/ui
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
export const Slider = <T,>({
|
||||||
|
rootRef,
|
||||||
|
items,
|
||||||
|
className,
|
||||||
|
preload = 1,
|
||||||
|
activeIndex = 0,
|
||||||
|
transitionDuration = 300,
|
||||||
|
transitionTimingFunction = 'cubic-bezier(.33,.36,0,1)',
|
||||||
|
itemRenderer,
|
||||||
|
...attrs
|
||||||
|
}: SliderProps<T>) => {
|
||||||
|
const count = items.length;
|
||||||
|
const unit = Math.floor(100 / count);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div ref={rootRef} className={clsx(className, styles.slider)} {...attrs}>
|
||||||
|
<div
|
||||||
|
className={styles.sliderContent}
|
||||||
|
style={{
|
||||||
|
width: `${items.length * 100}%`,
|
||||||
|
transform: `translateX(-${activeIndex * unit}%)`,
|
||||||
|
transition: `transform ${transitionDuration}ms ${transitionTimingFunction}`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{items?.map((item, index) => (
|
||||||
|
<div key={index} className={styles.slideItem}>
|
||||||
|
{preload === undefined || Math.abs(index - activeIndex) <= preload
|
||||||
|
? itemRenderer?.(item, index)
|
||||||
|
: null}
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
export interface BaseAIOnboardingDialogProps {
|
||||||
|
onDismiss: () => void;
|
||||||
|
}
|
||||||
|
export enum AIOnboardingType {
|
||||||
|
GENERAL = 'dismissAiOnboarding',
|
||||||
|
EDGELESS = 'dismissAiOnboardingEdgeless',
|
||||||
|
LOCAL = 'dismissAiOnboardingLocal',
|
||||||
|
}
|
||||||
14
packages/frontend/core/src/hooks/use-blur-root.ts
Normal file
14
packages/frontend/core/src/hooks/use-blur-root.ts
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export const useBlurRoot = (blur: boolean) => {
|
||||||
|
// blur modal background, can't use css: `backdrop-filter: blur()`,
|
||||||
|
// because it won't behave as expected on client side (texts over transparent window are not blurred)
|
||||||
|
useEffect(() => {
|
||||||
|
const appDom = document.querySelector('#app') as HTMLElement;
|
||||||
|
if (!appDom) return;
|
||||||
|
appDom.style.filter = blur ? 'blur(7px)' : 'none';
|
||||||
|
return () => {
|
||||||
|
appDom.style.filter = 'none';
|
||||||
|
};
|
||||||
|
}, [blur]);
|
||||||
|
};
|
||||||
@@ -22,6 +22,7 @@ import { matchPath } from 'react-router-dom';
|
|||||||
import { Map as YMap } from 'yjs';
|
import { Map as YMap } from 'yjs';
|
||||||
|
|
||||||
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
|
import { openQuickSearchModalAtom, openSettingModalAtom } from '../atoms';
|
||||||
|
import { AIOnboarding } from '../components/affine/ai-onboarding';
|
||||||
import { AppContainer } from '../components/affine/app-container';
|
import { AppContainer } from '../components/affine/app-container';
|
||||||
import { SyncAwareness } from '../components/affine/awareness';
|
import { SyncAwareness } from '../components/affine/awareness';
|
||||||
import {
|
import {
|
||||||
@@ -100,6 +101,8 @@ export const WorkspaceLayout = function WorkspaceLayout({
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
<Suspense fallback={<WorkspaceFallback />}>
|
<Suspense fallback={<WorkspaceFallback />}>
|
||||||
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
<WorkspaceLayoutInner>{children}</WorkspaceLayoutInner>
|
||||||
|
{/* should show after workspace loaded */}
|
||||||
|
<AIOnboarding />
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</SWRConfigProvider>
|
</SWRConfigProvider>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1279,5 +1279,20 @@
|
|||||||
"unnamed": "unnamed",
|
"unnamed": "unnamed",
|
||||||
"upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.",
|
"upgradeBrowser": "Please upgrade to the latest version of Chrome for the best experience.",
|
||||||
"will be moved to Trash": "{{title}} will be moved to Trash",
|
"will be moved to Trash": "{{title}} will be moved to Trash",
|
||||||
"will delete member": "will delete member"
|
"will delete member": "will delete member",
|
||||||
|
"com.affine.ai-onboarding.general.1.title": "Meet AFFiNE AI",
|
||||||
|
"com.affine.ai-onboarding.general.1.description": "Lets you think bigger, create faster, work smarter and save time for every project.",
|
||||||
|
"com.affine.ai-onboarding.general.2.title": "Chat with AFFiNE AI",
|
||||||
|
"com.affine.ai-onboarding.general.2.description": "Get instant insights to all your questions.",
|
||||||
|
"com.affine.ai-onboarding.general.3.title": "Edit Inline with AFFiNE AI",
|
||||||
|
"com.affine.ai-onboarding.general.3.description": "Perfect tone, spelling, and summaries in seconds.",
|
||||||
|
"com.affine.ai-onboarding.general.4.title": "Make it Real with AFFiNE AI",
|
||||||
|
"com.affine.ai-onboarding.general.4.description": "From concept to completion, turn ideas into reality.",
|
||||||
|
"com.affine.ai-onboarding.general.5.title": "AFFiNE AI is ready",
|
||||||
|
"com.affine.ai-onboarding.general.5.description": "Go to <a>{{link}}</a> for learn more details about AFFiNE AI.",
|
||||||
|
"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.purchase": "Get Unlimited Usage"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,6 +36,11 @@ export const test = base.extend<{
|
|||||||
}>({
|
}>({
|
||||||
page: async ({ electronApp }, use) => {
|
page: async ({ electronApp }, use) => {
|
||||||
const page = await electronApp.firstWindow();
|
const page = await electronApp.firstWindow();
|
||||||
|
await page.evaluate(() => {
|
||||||
|
window.localStorage.setItem('dismissAiOnboarding', 'true');
|
||||||
|
window.localStorage.setItem('dismissAiOnboardingEdgeless', 'true');
|
||||||
|
window.localStorage.setItem('dismissAiOnboardingLocal', 'true');
|
||||||
|
});
|
||||||
// wait for blocksuite to be loaded
|
// wait for blocksuite to be loaded
|
||||||
await page.waitForSelector('v-line');
|
await page.waitForSelector('v-line');
|
||||||
if (enableCoverage) {
|
if (enableCoverage) {
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ type CurrentDocCollection = {
|
|||||||
export const skipOnboarding = async (context: BrowserContext) => {
|
export const skipOnboarding = async (context: BrowserContext) => {
|
||||||
await context.addInitScript(() => {
|
await context.addInitScript(() => {
|
||||||
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
||||||
|
window.localStorage.setItem('dismissAiOnboarding', 'true');
|
||||||
|
window.localStorage.setItem('dismissAiOnboardingEdgeless', 'true');
|
||||||
|
window.localStorage.setItem('dismissAiOnboardingLocal', 'true');
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ localStorage.clear();
|
|||||||
|
|
||||||
// do not show onboarding for storybook
|
// do not show onboarding for storybook
|
||||||
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
window.localStorage.setItem('app_config', '{"onBoarding":false}');
|
||||||
|
window.localStorage.setItem('dismissAiOnboarding', 'true');
|
||||||
|
window.localStorage.setItem('dismissAiOnboardingEdgeless', 'true');
|
||||||
|
window.localStorage.setItem('dismissAiOnboardingLocal', 'true');
|
||||||
|
|
||||||
const services = new ServiceCollection();
|
const services = new ServiceCollection();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user