Skip to content

Commit

Permalink
feat(core): add ai usage in account-setting
Browse files Browse the repository at this point in the history
  • Loading branch information
CatsJuice committed Apr 11, 2024
1 parent c92bec0 commit 6e06e7e
Show file tree
Hide file tree
Showing 14 changed files with 419 additions and 285 deletions.
1 change: 1 addition & 0 deletions packages/frontend/core/src/atoms/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export type SettingAtom = Pick<
'activeTab' | 'workspaceMetadata'
> & {
open: boolean;
scrollAnchor?: string;
};

export const openSettingModalAtom = atom<SettingAtom>({
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
);
};
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { useMutation } from '../../../../hooks/use-mutation';
import { mixpanel } from '../../../../utils';
import { validateAndReduceImage } from '../../../../utils/reduce-image';
import { Upload } from '../../../pure/file-upload';
import { AIUsagePanel } from './ai-usage-panel';
import { StorageProgress } from './storage-progress';
import * as styles from './style.css';

Expand Down Expand Up @@ -256,6 +257,9 @@ export const AccountSetting: FC = () => {
<Suspense>
<StoragePanel />
</Suspense>
<Suspense>
<AIUsagePanel />
</Suspense>
<SettingRow
name={t[`Sign out`]()}
desc={t['com.affine.setting.sign.out.message']()}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ export const AIResume = ({
onSuccess: data => {
// refresh idempotency key
setIdempotencyKey(nanoid());
onSubscriptionUpdate(data.resumeSubscription);
onSubscriptionUpdate?.(data.resumeSubscription);
notify({
icon: (
<SingleSelectSelectSolidIcon
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ import { popupWindow } from '@affine/core/utils';
import {
createCheckoutSessionMutation,
SubscriptionPlan,
SubscriptionRecurring,
} from '@affine/graphql';
import { assertExists } from '@blocksuite/global/utils';
import { nanoid } from 'nanoid';
import { useCallback, useEffect, useMemo, useRef } from 'react';

Expand All @@ -16,10 +18,11 @@ export interface AISubscribeProps extends BaseActionProps, ButtonProps {}

export const AISubscribe = ({
price,
recurring,
recurring = SubscriptionRecurring.Yearly,
onSubscriptionUpdate,
...btnProps
}: AISubscribeProps) => {
assertExists(price);
const idempotencyKey = useMemo(() => `${nanoid()}-${recurring}`, [recurring]);
const { priceReadable, priceFrequency } = useAffineAIPrice(price);

Expand All @@ -31,7 +34,7 @@ export const AISubscribe = ({

const onClose = useCallback(() => {
newTabRef.current = null;
onSubscriptionUpdate();
onSubscriptionUpdate?.();
}, [onSubscriptionUpdate]);

useEffect(() => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type { SubscriptionMutator } from '@affine/core/hooks/use-subscription';
import type { PricesQuery, SubscriptionRecurring } from '@affine/graphql';

export interface BaseActionProps {
price: PricesQuery['prices'][number];
recurring: SubscriptionRecurring;
onSubscriptionUpdate: SubscriptionMutator;
price?: PricesQuery['prices'][number];
recurring?: SubscriptionRecurring;
onSubscriptionUpdate?: SubscriptionMutator;
}
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Divider, IconButton } from '@affine/component';
import { SettingHeader } from '@affine/component/setting-components';
import { openSettingModalAtom } from '@affine/core/atoms';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightBigIcon, ArrowUpSmallIcon } from '@blocksuite/icons';
import * as Collapsible from '@radix-ui/react-collapsible';
import * as ScrollArea from '@radix-ui/react-scroll-area';
import { useAtom } from 'jotai';
import {
type HtmlHTMLAttributes,
type PropsWithChildren,
type ReactNode,
useCallback,
useLayoutEffect,
useRef,
useState,
} from 'react';

Expand Down Expand Up @@ -70,6 +74,20 @@ export interface PlanLayoutProps {

export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
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 (
<div className={styles.plansLayoutRoot}>
{/* TODO: SettingHeader component shouldn't have margin itself */}
Expand All @@ -81,7 +99,9 @@ export const PlanLayout = ({ cloud, ai }: PlanLayoutProps) => {
{ai ? (
<>
<Divider />
{ai}
<div ref={aiPricingPlanRef} id="aiPricingPlan">
{ai}
</div>
</>
) : null}
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import { WorkspaceSetting } from './workspace-setting';

export interface SettingProps extends ModalProps {
activeTab: ActiveTab;
scrollAnchor?: string;
workspaceMetadata?: WorkspaceMetadata | null;
onSettingClick: (params: {
activeTab: ActiveTab;
Expand Down Expand Up @@ -107,6 +108,7 @@ const SettingModalInner = ({
onTabChange={onTabChange}
selectedWorkspaceId={workspaceMetadata?.id ?? null}
/>
{/* TODO: use scroll-area from radix */}
<div
data-testid="setting-modal-content"
className={style.wrapper}
Expand Down
7 changes: 5 additions & 2 deletions packages/frontend/core/src/providers/modal-provider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -90,8 +90,10 @@ const HistoryTipsModal = lazy(() =>
);

export const Setting = () => {
const [{ open, workspaceMetadata, activeTab }, setOpenSettingModalAtom] =
useAtom(openSettingModalAtom);
const [
{ open, workspaceMetadata, activeTab, scrollAnchor },
setOpenSettingModalAtom,
] = useAtom(openSettingModalAtom);

const onSettingClick = useCallback(
({
Expand Down Expand Up @@ -134,6 +136,7 @@ export const Setting = () => {
<SettingModal
open={open}
activeTab={activeTab}
scrollAnchor={scrollAnchor}
workspaceMetadata={workspaceMetadata}
onSettingClick={onSettingClick}
onOpenChange={onOpenChange}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
query getCopilotQuota($workspaceId: String!, $docId: String!) {
query getCopilotQuota {
currentUser {
copilot {
quota {
Expand Down
2 changes: 1 addition & 1 deletion packages/frontend/graphql/src/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -281,7 +281,7 @@ export const getCopilotQuotaQuery = {
definitionName: 'currentUser',
containsFile: false,
query: `
query getCopilotQuota($workspaceId: String!, $docId: String!) {
query getCopilotQuota {
currentUser {
copilot {
quota {
Expand Down
5 changes: 1 addition & 4 deletions packages/frontend/graphql/src/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -369,10 +369,7 @@ export type GetCopilotHistoriesQuery = {
} | null;
};

export type GetCopilotQuotaQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
docId: Scalars['String']['input'];
}>;
export type GetCopilotQuotaQueryVariables = Exact<{ [key: string]: never }>;

export type GetCopilotQuotaQuery = {
__typename?: 'Query';
Expand Down
6 changes: 6 additions & 0 deletions packages/frontend/i18n/src/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -910,6 +910,12 @@
"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-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-2": "Unlimited login devices",
"com.affine.payment.benefit-3": "Unlimited blocks",
Expand Down

0 comments on commit 6e06e7e

Please sign in to comment.