mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
feat(core): add ai-usage info in sidebar user avatar menu (#7294)
This commit is contained in:
@@ -25,6 +25,7 @@ export const Divider = forwardRef<HTMLDivElement, DividerProps>(
|
||||
) => {
|
||||
return (
|
||||
<div
|
||||
data-divider
|
||||
ref={ref}
|
||||
className={clsx(
|
||||
styles.divider,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Tooltip } from '@affine/component/ui/tooltip';
|
||||
import { mixpanel } from '@affine/core/utils';
|
||||
import { SubscriptionPlan } from '@affine/graphql';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { useLiveData, useServices } from '@toeverything/infra';
|
||||
import { useSetAtom } from 'jotai';
|
||||
@@ -61,15 +62,12 @@ export const UserPlanButton = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!plan) {
|
||||
// no plan, do nothing
|
||||
return;
|
||||
}
|
||||
const planLabel = plan ?? SubscriptionPlan.Free;
|
||||
|
||||
return (
|
||||
<Tooltip content={t['com.affine.payment.tag-tooltips']()} side="top">
|
||||
<div className={styles.userPlanButton} onClick={handleClick}>
|
||||
{plan}
|
||||
{planLabel}
|
||||
</div>
|
||||
</Tooltip>
|
||||
);
|
||||
|
||||
@@ -32,21 +32,46 @@ export const operationMenu = style({
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
});
|
||||
// TODO: refactor menu, use `gap` to replace `margin`
|
||||
globalStyle(`.${operationMenu} > div:not([data-divider])`, {
|
||||
marginBottom: '0 !important',
|
||||
marginTop: '0 !important',
|
||||
});
|
||||
|
||||
export const cloudUsage = style({
|
||||
export const usageBlock = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: 4,
|
||||
borderRadius: 4,
|
||||
});
|
||||
export const aiUsageBlock = style({
|
||||
padding: 12,
|
||||
cursor: 'pointer',
|
||||
':hover': {
|
||||
background: cssVar('hoverColor'),
|
||||
},
|
||||
selectors: {
|
||||
'&[data-pro]': {
|
||||
padding: '12px 12px 2px 12px',
|
||||
},
|
||||
},
|
||||
});
|
||||
export const cloudUsageBlock = style({
|
||||
padding: '4px 12px',
|
||||
});
|
||||
export const cloudUsageLabel = style({
|
||||
fontWeight: 500,
|
||||
|
||||
export const usageLabel = style({
|
||||
fontWeight: 400,
|
||||
lineHeight: '20px',
|
||||
fontSize: cssVar('fontXs'),
|
||||
color: cssVar('textSecondaryColor'),
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
});
|
||||
export const cloudUsageLabelUsed = style({
|
||||
color: progressColorVar,
|
||||
export const usageLabelTitle = style({
|
||||
color: cssVar('textPrimaryColor'),
|
||||
marginRight: '0.5em',
|
||||
});
|
||||
|
||||
export const cloudUsageBar = style({
|
||||
@@ -54,7 +79,7 @@ export const cloudUsageBar = style({
|
||||
borderRadius: 5,
|
||||
overflow: 'hidden',
|
||||
position: 'relative',
|
||||
minWidth: 160,
|
||||
minWidth: 260,
|
||||
|
||||
'::before': {
|
||||
position: 'absolute',
|
||||
@@ -69,3 +94,13 @@ export const cloudUsageBarInner = style({
|
||||
borderRadius: 'inherit',
|
||||
backgroundColor: progressColorVar,
|
||||
});
|
||||
export const freeTag = style({
|
||||
height: 20,
|
||||
padding: '0px 4px',
|
||||
borderRadius: 4,
|
||||
fontWeight: 500,
|
||||
fontSize: cssVar('fontXs'),
|
||||
lineHeight: '20px',
|
||||
color: cssVar('pureWhite'),
|
||||
background: cssVar('primaryColor'),
|
||||
});
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
Menu,
|
||||
MenuIcon,
|
||||
MenuItem,
|
||||
type MenuProps,
|
||||
Skeleton,
|
||||
} from '@affine/component';
|
||||
import {
|
||||
@@ -18,15 +19,21 @@ import { mixpanel } from '@affine/core/utils';
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { AccountIcon, SignOutIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVar } from '@toeverything/theme';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import clsx from 'clsx';
|
||||
import { useSetAtom } from 'jotai';
|
||||
import { useCallback, useEffect } from 'react';
|
||||
|
||||
import {
|
||||
type AuthAccountInfo,
|
||||
AuthService,
|
||||
ServerConfigService,
|
||||
SubscriptionService,
|
||||
UserCopilotQuotaService,
|
||||
UserQuotaService,
|
||||
} from '../../modules/cloud';
|
||||
import { UserPlanButton } from '../affine/auth/user-plan-button';
|
||||
import * as styles from './index.css';
|
||||
import { UnknownUserIcon } from './unknow-user';
|
||||
|
||||
@@ -40,9 +47,12 @@ export const UserInfo = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const menuContentOptions: MenuProps['contentOptions'] = {
|
||||
className: styles.operationMenu,
|
||||
};
|
||||
const AuthorizedUserInfo = ({ account }: { account: AuthAccountInfo }) => {
|
||||
return (
|
||||
<Menu items={<OperationMenu />}>
|
||||
<Menu items={<OperationMenu />} contentOptions={menuContentOptions}>
|
||||
<Button
|
||||
data-testid="sidebar-user-avatar"
|
||||
type="plain"
|
||||
@@ -112,7 +122,6 @@ const AccountMenu = () => {
|
||||
>
|
||||
{t['com.affine.workspace.cloud.account.settings']()}
|
||||
</MenuItem>
|
||||
<Divider />
|
||||
<MenuItem
|
||||
preFix={
|
||||
<MenuIcon>
|
||||
@@ -129,6 +138,7 @@ const AccountMenu = () => {
|
||||
};
|
||||
|
||||
const CloudUsage = () => {
|
||||
const t = useI18n();
|
||||
const quota = useService(UserQuotaService).quota;
|
||||
const quotaError = useLiveData(quota.error$);
|
||||
|
||||
@@ -155,15 +165,126 @@ const CloudUsage = () => {
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.cloudUsage}
|
||||
className={clsx(styles.usageBlock, styles.cloudUsageBlock)}
|
||||
style={assignInlineVars({
|
||||
[styles.progressColorVar]: color,
|
||||
})}
|
||||
>
|
||||
<div className={styles.cloudUsageLabel}>
|
||||
<span className={styles.cloudUsageLabelUsed}>{usedFormatted}</span>
|
||||
<span> / </span>
|
||||
<span>{maxFormatted}</span>
|
||||
<div className={styles.usageLabel}>
|
||||
<div>
|
||||
<span className={styles.usageLabelTitle}>
|
||||
{t['com.affine.user-info.usage.cloud']()}
|
||||
</span>
|
||||
<span>{usedFormatted}</span>
|
||||
<span> / </span>
|
||||
<span>{maxFormatted}</span>
|
||||
</div>
|
||||
<UserPlanButton />
|
||||
</div>
|
||||
|
||||
<div className={styles.cloudUsageBar}>
|
||||
<div
|
||||
className={styles.cloudUsageBarInner}
|
||||
style={{ width: `${percent}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const AIUsage = () => {
|
||||
const t = useI18n();
|
||||
const copilotQuotaService = useService(UserCopilotQuotaService);
|
||||
const subscriptionService = useService(SubscriptionService);
|
||||
|
||||
useEffect(() => {
|
||||
// revalidate latest subscription status
|
||||
subscriptionService.subscription.revalidate();
|
||||
}, [subscriptionService]);
|
||||
useEffect(() => {
|
||||
copilotQuotaService.copilotQuota.revalidate();
|
||||
}, [copilotQuotaService]);
|
||||
|
||||
const copilotActionLimit = useLiveData(
|
||||
copilotQuotaService.copilotQuota.copilotActionLimit$
|
||||
);
|
||||
const copilotActionUsed = useLiveData(
|
||||
copilotQuotaService.copilotQuota.copilotActionUsed$
|
||||
);
|
||||
const loading = copilotActionLimit === null || copilotActionUsed === null;
|
||||
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
|
||||
|
||||
const setSettingModalAtom = useSetAtom(openSettingModalAtom);
|
||||
|
||||
const goToAIPlanPage = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'plans',
|
||||
scrollAnchor: 'aiPricingPlan',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
const goToAccountSetting = useCallback(() => {
|
||||
setSettingModalAtom({
|
||||
open: true,
|
||||
activeTab: 'account',
|
||||
});
|
||||
}, [setSettingModalAtom]);
|
||||
|
||||
if (loading) {
|
||||
if (loadError) console.error(loadError);
|
||||
return null;
|
||||
}
|
||||
|
||||
// unlimited
|
||||
if (copilotActionLimit === 'unlimited') {
|
||||
return (
|
||||
<div
|
||||
onClick={goToAccountSetting}
|
||||
data-pro
|
||||
className={clsx(styles.usageBlock, styles.aiUsageBlock)}
|
||||
>
|
||||
<div className={styles.usageLabel}>
|
||||
<div className={styles.usageLabelTitle}>
|
||||
{t['com.affine.user-info.usage.ai']()}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.usageLabel}>
|
||||
{t['com.affine.payment.ai.usage-description-purchased']()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const percent = Math.min(
|
||||
100,
|
||||
Math.max(
|
||||
0.5,
|
||||
Number(((copilotActionUsed / copilotActionLimit) * 100).toFixed(4))
|
||||
)
|
||||
);
|
||||
|
||||
const color = percent > 80 ? cssVar('errorColor') : cssVar('processingColor');
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={goToAIPlanPage}
|
||||
className={clsx(styles.usageBlock, styles.aiUsageBlock)}
|
||||
style={assignInlineVars({
|
||||
[styles.progressColorVar]: color,
|
||||
})}
|
||||
>
|
||||
<div className={styles.usageLabel}>
|
||||
<div>
|
||||
<span className={styles.usageLabelTitle}>
|
||||
{t['com.affine.user-info.usage.ai']()}
|
||||
</span>
|
||||
<span>{copilotActionUsed}</span>
|
||||
<span> / </span>
|
||||
<span>{copilotActionLimit}</span>
|
||||
</div>
|
||||
|
||||
<div className={styles.freeTag}>Free</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.cloudUsageBar}>
|
||||
@@ -177,8 +298,14 @@ const CloudUsage = () => {
|
||||
};
|
||||
|
||||
const OperationMenu = () => {
|
||||
const serverConfigService = useService(ServerConfigService);
|
||||
const serverFeatures = useLiveData(
|
||||
serverConfigService.serverConfig.features$
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{serverFeatures?.copilot ? <AIUsage /> : null}
|
||||
<CloudUsage />
|
||||
<Divider />
|
||||
<AccountMenu />
|
||||
|
||||
@@ -1340,5 +1340,7 @@
|
||||
"unnamed": "unnamed",
|
||||
"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 delete member": "will delete member"
|
||||
"will delete member": "will delete member",
|
||||
"com.affine.user-info.usage.ai": "AI Usage",
|
||||
"com.affine.user-info.usage.cloud": "Cloud Storage"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user