feat(core): add ai usage in account-setting (#6516)

This commit is contained in:
CatsJuice
2024-04-12 06:06:11 +00:00
parent 244e3fd71b
commit d40052c748
12 changed files with 172 additions and 14 deletions

View File

@@ -20,6 +20,7 @@ export type SettingAtom = Pick<
'activeTab' | 'workspaceMetadata' 'activeTab' | 'workspaceMetadata'
> & { > & {
open: boolean; open: boolean;
scrollAnchor?: string;
}; };
export const openSettingModalAtom = atom<SettingAtom>({ export const openSettingModalAtom = atom<SettingAtom>({

View File

@@ -0,0 +1,124 @@
import { Button } from '@affine/component';
import { SettingRow } from '@affine/component/setting-components';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useQuery } from '@affine/core/hooks/use-query';
import { useUserSubscription } from '@affine/core/hooks/use-subscription';
import {
getCopilotQuotaQuery,
pricesQuery,
SubscriptionPlan,
SubscriptionRecurring,
} from '@affine/graphql';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils';
import { cssVar } from '@toeverything/theme';
import { useSetAtom } from 'jotai';
import { useCallback } from 'react';
import { useAffineAISubscription } from '../general-setting/plans/ai/use-affine-ai-subscription';
import * as styles from './storage-progress.css';
export const AIUsagePanel = () => {
const t = useAFFiNEI18N();
const setOpenSettingModal = useSetAtom(openSettingModalAtom);
const [, mutateSubscription] = useUserSubscription();
const { actionType, Action } = useAffineAISubscription();
const openAiPricingPlan = useCallback(() => {
setOpenSettingModal({
open: true,
activeTab: 'plans',
scrollAnchor: 'aiPricingPlan',
});
}, [setOpenSettingModal]);
if (actionType === 'cancel') {
return (
<SettingRow
desc={t['com.affine.payment.ai.usage-description-purchased']()}
name={t['com.affine.payment.ai.usage-title']()}
>
<Button onClick={openAiPricingPlan}>
{t['com.affine.payment.ai.usage.change-button-label']()}
</Button>
</SettingRow>
);
}
if (actionType === 'resume') {
return (
<SettingRow
desc={t['com.affine.payment.ai.usage-description-purchased']()}
name={t['com.affine.payment.ai.usage-title']()}
>
<Action onSubscriptionUpdate={mutateSubscription} />
</SettingRow>
);
}
return <AIUsagePanelNotSubscripted />;
};
export const AIUsagePanelNotSubscripted = () => {
const t = useAFFiNEI18N();
const [, mutateSubscription] = useUserSubscription();
const { actionType, Action } = useAffineAISubscription();
const {
data: { prices },
} = useQuery({ query: pricesQuery });
const { data: quota } = useQuery({
query: getCopilotQuotaQuery,
});
const { limit = 10, used = 0 } = quota.currentUser?.copilot.quota || {};
const percent = Math.min(
100,
Math.max(0.5, Number(((used / limit) * 100).toFixed(4)))
);
const price = prices.find(p => p.plan === SubscriptionPlan.AI);
assertExists(price);
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
return (
<SettingRow
spreadCol={false}
desc=""
name={t['com.affine.payment.ai.usage-title']()}
>
<div className={styles.storageProgressContainer}>
<div className={styles.storageProgressWrapper}>
<div className="storage-progress-desc">
<span>{t['com.affine.payment.ai.usage.used-caption']()}</span>
<span>
{t['com.affine.payment.ai.usage.used-detail']({
used: used.toString(),
limit: limit.toString(),
})}
</span>
</div>
<div className="storage-progress-bar-wrapper">
<div
className={styles.storageProgressBar}
style={{ width: `${percent}%`, backgroundColor: color }}
></div>
</div>
</div>
<Action
recurring={SubscriptionRecurring.Yearly}
onSubscriptionUpdate={mutateSubscription}
price={price}
type="primary"
className={styles.storageButton}
>
{actionType === 'subscribe'
? t['com.affine.payment.ai.usage.purchase-button-label']()
: null}
</Action>
</div>
</SettingRow>
);
};

View File

@@ -5,6 +5,7 @@ import {
} from '@affine/component/setting-components'; } from '@affine/component/setting-components';
import { Avatar } from '@affine/component/ui/avatar'; import { Avatar } from '@affine/component/ui/avatar';
import { Button } from '@affine/component/ui/button'; import { Button } from '@affine/component/ui/button';
import { SWRErrorBoundary } from '@affine/core/components/pure/swr-error-bundary';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { import {
removeAvatarMutation, removeAvatarMutation,
@@ -28,6 +29,7 @@ import { useMutation } from '../../../../hooks/use-mutation';
import { mixpanel } from '../../../../utils'; import { mixpanel } from '../../../../utils';
import { validateAndReduceImage } from '../../../../utils/reduce-image'; import { validateAndReduceImage } from '../../../../utils/reduce-image';
import { Upload } from '../../../pure/file-upload'; import { Upload } from '../../../pure/file-upload';
import { AIUsagePanel } from './ai-usage-panel';
import { StorageProgress } from './storage-progress'; import { StorageProgress } from './storage-progress';
import * as styles from './style.css'; import * as styles from './style.css';
@@ -256,6 +258,11 @@ export const AccountSetting: FC = () => {
<Suspense> <Suspense>
<StoragePanel /> <StoragePanel />
</Suspense> </Suspense>
<SWRErrorBoundary fallback={<div />}>
<Suspense>
<AIUsagePanel />
</Suspense>
</SWRErrorBoundary>
<SettingRow <SettingRow
name={t[`Sign out`]()} name={t[`Sign out`]()}
desc={t['com.affine.setting.sign.out.message']()} desc={t['com.affine.setting.sign.out.message']()}

View File

@@ -43,7 +43,7 @@ export const AICancel = ({
onSuccess: data => { onSuccess: data => {
// refresh idempotency key // refresh idempotency key
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
onSubscriptionUpdate(data.cancelSubscription); onSubscriptionUpdate?.(data.cancelSubscription);
}, },
} }
); );

View File

@@ -48,7 +48,7 @@ export const AIResume = ({
onSuccess: data => { onSuccess: data => {
// refresh idempotency key // refresh idempotency key
setIdempotencyKey(nanoid()); setIdempotencyKey(nanoid());
onSubscriptionUpdate(data.resumeSubscription); onSubscriptionUpdate?.(data.resumeSubscription);
notify({ notify({
icon: ( icon: (
<SingleSelectSelectSolidIcon <SingleSelectSelectSolidIcon

View File

@@ -5,7 +5,9 @@ import { popupWindow } from '@affine/core/utils';
import { import {
createCheckoutSessionMutation, createCheckoutSessionMutation,
SubscriptionPlan, SubscriptionPlan,
SubscriptionRecurring,
} from '@affine/graphql'; } from '@affine/graphql';
import { assertExists } from '@blocksuite/global/utils';
import { nanoid } from 'nanoid'; import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useRef } from 'react'; import { useCallback, useEffect, useMemo, useRef } from 'react';
@@ -16,10 +18,11 @@ export interface AISubscribeProps extends BaseActionProps, ButtonProps {}
export const AISubscribe = ({ export const AISubscribe = ({
price, price,
recurring, recurring = SubscriptionRecurring.Yearly,
onSubscriptionUpdate, onSubscriptionUpdate,
...btnProps ...btnProps
}: AISubscribeProps) => { }: AISubscribeProps) => {
assertExists(price);
const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]); const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
const { priceReadable, priceFrequency } = useAffineAIPrice(price); const { priceReadable, priceFrequency } = useAffineAIPrice(price);
@@ -31,7 +34,7 @@ export const AISubscribe = ({
const onClose = useCallback(() => { const onClose = useCallback(() => {
newTabRef.current = null; newTabRef.current = null;
onSubscriptionUpdate(); onSubscriptionUpdate?.();
}, [onSubscriptionUpdate]); }, [onSubscriptionUpdate]);
useEffect(() => { useEffect(() => {

View File

@@ -2,7 +2,7 @@ import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
import type { PricesQuery, SubscriptionRecurring } from '@affine/graphql'; import type { PricesQuery, SubscriptionRecurring } from '@affine/graphql';
export interface BaseActionProps { export interface BaseActionProps {
price: PricesQuery['prices'][number]; price?: PricesQuery['prices'][number];
recurring: SubscriptionRecurring; recurring?: SubscriptionRecurring;
onSubscriptionUpdate: SubscriptionMutator; onSubscriptionUpdate?: SubscriptionMutator;
} }

View File

@@ -1,14 +1,18 @@
import { Divider, IconButton } from '@affine/component'; import { Divider, IconButton } from '@affine/component';
import { SettingHeader } from '@affine/component/setting-components'; import { SettingHeader } from '@affine/component/setting-components';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon, ArrowUpSmallIcon } from '@blocksuite/icons'; import { ArrowRightBigIcon, ArrowUpSmallIcon } from '@blocksuite/icons';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import * as ScrollArea from '@radix-ui/react-scroll-area'; import * as ScrollArea from '@radix-ui/react-scroll-area';
import { useAtom } from 'jotai';
import { import {
type HtmlHTMLAttributes, type HtmlHTMLAttributes,
type PropsWithChildren, type PropsWithChildren,
type ReactNode, type ReactNode,
useCallback, useCallback,
useLayoutEffect,
useRef,
useState, useState,
} from 'react'; } from 'react';
@@ -70,6 +74,20 @@ export interface PlanLayoutProps {
export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => { export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const [{ scrollAnchor }, setOpenSettingModal] = useAtom(openSettingModalAtom);
const aiPricingPlanRef = useRef<HTMLDivElement>(null);
// TODO: Need a better solution to handle this situation
useLayoutEffect(() => {
if (!scrollAnchor) return;
setTimeout(() => {
if (scrollAnchor === 'aiPricingPlan' && aiPricingPlanRef.current) {
aiPricingPlanRef.current.scrollIntoView();
setOpenSettingModal(prev => ({ ...prev, scrollAnchor: undefined }));
}
});
}, [scrollAnchor, setOpenSettingModal]);
return ( return (
<div className={styles.plansLayoutRoot}> <div className={styles.plansLayoutRoot}>
{/* TODO: SettingHeader component shouldn't have margin itself */} {/* TODO: SettingHeader component shouldn't have margin itself */}
@@ -81,7 +99,9 @@ export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
{ai ? ( {ai ? (
<> <>
<Divider /> <Divider />
{ai} <div ref={aiPricingPlanRef} id="aiPricingPlan">
{ai}
</div>
</> </>
) : null} ) : null}
</div> </div>

View File

@@ -1,4 +1,4 @@
query getCopilotQuota($workspaceId: String!, $docId: String!) { query getCopilotQuota {
currentUser { currentUser {
copilot { copilot {
quota { quota {

View File

@@ -281,7 +281,7 @@ export const getCopilotQuotaQuery = {
definitionName: 'currentUser', definitionName: 'currentUser',
containsFile: false, containsFile: false,
query: ` query: `
query getCopilotQuota($workspaceId: String!, $docId: String!) { query getCopilotQuota {
currentUser { currentUser {
copilot { copilot {
quota { quota {

View File

@@ -369,10 +369,7 @@ export type GetCopilotHistoriesQuery = {
} | null; } | null;
}; };
export type GetCopilotQuotaQueryVariables = Exact<{ export type GetCopilotQuotaQueryVariables = Exact<{ [key: string]: never }>;
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
}>;
export type GetCopilotQuotaQuery = { export type GetCopilotQuotaQuery = {
__typename?: 'Query'; __typename?: 'Query';

View File

@@ -910,6 +910,12 @@
"com.affine.payment.ai.benefit.g3-1": "Memorize and tidy up your knowledge", "com.affine.payment.ai.benefit.g3-1": "Memorize and tidy up your knowledge",
"com.affine.payment.ai.benefit.g3-2": "Auto-sorting and auto-tagging", "com.affine.payment.ai.benefit.g3-2": "Auto-sorting and auto-tagging",
"com.affine.payment.ai.benefit.g3-3": "Open source & Privacy ensured", "com.affine.payment.ai.benefit.g3-3": "Open source & Privacy ensured",
"com.affine.payment.ai.usage-title": "AFFiNE AI Usage",
"com.affine.payment.ai.usage-description-purchased": "You have purchased AFFiNE AI.",
"com.affine.payment.ai.usage.change-button-label": "Upgraded",
"com.affine.payment.ai.usage.purchase-button-label": "Upgrade",
"com.affine.payment.ai.usage.used-caption": "Times used",
"com.affine.payment.ai.usage.used-detail": "{{used}}/{{limit}} Times",
"com.affine.payment.benefit-1": "Unlimited local workspaces", "com.affine.payment.benefit-1": "Unlimited local workspaces",
"com.affine.payment.benefit-2": "Unlimited login devices", "com.affine.payment.benefit-2": "Unlimited login devices",
"com.affine.payment.benefit-3": "Unlimited blocks", "com.affine.payment.benefit-3": "Unlimited blocks",