fix(core): fix error when server not support ai (#6796)

This commit is contained in:
EYHN
2024-05-07 08:25:27 +00:00
parent a0e0b6b53b
commit 35ce4adffe
13 changed files with 226 additions and 97 deletions
@@ -4,7 +4,7 @@ import { openSettingModalAtom } from '@affine/core/atoms';
import { import {
ServerConfigService, ServerConfigService,
SubscriptionService, SubscriptionService,
UserQuotaService, UserCopilotQuotaService,
} from '@affine/core/modules/cloud'; } from '@affine/core/modules/cloud';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
@@ -28,14 +28,18 @@ export const AIUsagePanel = () => {
// revalidate latest subscription status // revalidate latest subscription status
subscriptionService.subscription.revalidate(); subscriptionService.subscription.revalidate();
}, [subscriptionService]); }, [subscriptionService]);
const quotaService = useService(UserQuotaService); const copilotQuotaService = useService(UserCopilotQuotaService);
useEffect(() => { useEffect(() => {
quotaService.quota.revalidate(); copilotQuotaService.copilotQuota.revalidate();
}, [quotaService]); }, [copilotQuotaService]);
const aiActionLimit = useLiveData(quotaService.quota.aiActionLimit$); const copilotActionLimit = useLiveData(
const aiActionUsed = useLiveData(quotaService.quota.aiActionUsed$); copilotQuotaService.copilotQuota.copilotActionLimit$
const loading = aiActionLimit === null || aiActionUsed === null; );
const loadError = useLiveData(quotaService.quota.error$); const copilotActionUsed = useLiveData(
copilotQuotaService.copilotQuota.copilotActionUsed$
);
const loading = copilotActionLimit === null || copilotActionUsed === null;
const loadError = useLiveData(copilotQuotaService.copilotQuota.error$);
const openBilling = useCallback(() => { const openBilling = useCallback(() => {
setOpenSettingModal({ setOpenSettingModal({
@@ -69,13 +73,13 @@ export const AIUsagePanel = () => {
} }
const percent = const percent =
aiActionLimit === 'unlimited' copilotActionLimit === 'unlimited'
? 0 ? 0
: Math.min( : Math.min(
100, 100,
Math.max( Math.max(
0.5, 0.5,
Number(((aiActionUsed / aiActionLimit) * 100).toFixed(4)) Number(((copilotActionUsed / copilotActionLimit) * 100).toFixed(4))
) )
); );
@@ -91,7 +95,7 @@ export const AIUsagePanel = () => {
} }
name={t['com.affine.payment.ai.usage-title']()} name={t['com.affine.payment.ai.usage-title']()}
> >
{aiActionLimit === 'unlimited' ? ( {copilotActionLimit === 'unlimited' ? (
hasPaymentFeature && aiSubscription?.canceledAt ? ( hasPaymentFeature && aiSubscription?.canceledAt ? (
<AIResume /> <AIResume />
) : ( ) : (
@@ -106,8 +110,8 @@ export const AIUsagePanel = () => {
<span>{t['com.affine.payment.ai.usage.used-caption']()}</span> <span>{t['com.affine.payment.ai.usage.used-caption']()}</span>
<span> <span>
{t['com.affine.payment.ai.usage.used-detail']({ {t['com.affine.payment.ai.usage.used-detail']({
used: aiActionUsed.toString(), used: copilotActionUsed.toString(),
limit: aiActionLimit.toString(), limit: copilotActionLimit.toString(),
})} })}
</span> </span>
</div> </div>
@@ -8,7 +8,12 @@ import { Button } from '@affine/component/ui/button';
import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons'; import { ArrowRightSmallIcon, CameraIcon } from '@blocksuite/icons';
import { useEnsureLiveData, useService } from '@toeverything/infra'; import {
useEnsureLiveData,
useLiveData,
useService,
useServices,
} from '@toeverything/infra';
import { useSetAtom } from 'jotai'; import { useSetAtom } from 'jotai';
import type { FC, MouseEvent } from 'react'; import type { FC, MouseEvent } from 'react';
import { useCallback, useEffect, useState } from 'react'; import { useCallback, useEffect, useState } from 'react';
@@ -18,7 +23,7 @@ import {
openSettingModalAtom, openSettingModalAtom,
openSignOutModalAtom, openSignOutModalAtom,
} from '../../../../atoms'; } from '../../../../atoms';
import { AuthService } from '../../../../modules/cloud'; import { AuthService, ServerConfigService } from '../../../../modules/cloud';
import { mixpanel } from '../../../../utils'; import { mixpanel } from '../../../../utils';
import { Upload } from '../../../pure/file-upload'; import { Upload } from '../../../pure/file-upload';
import { AIUsagePanel } from './ai-usage-panel'; import { AIUsagePanel } from './ai-usage-panel';
@@ -178,8 +183,15 @@ const StoragePanel = () => {
}; };
export const AccountSetting: FC = () => { export const AccountSetting: FC = () => {
const { authService, serverConfigService } = useServices({
AuthService,
ServerConfigService,
});
const serverFeatures = useLiveData(
serverConfigService.serverConfig.features$
);
const t = useAFFiNEI18N(); const t = useAFFiNEI18N();
const session = useService(AuthService).session; const session = authService.session;
useEffect(() => { useEffect(() => {
session.revalidate(); session.revalidate();
}, [session]); }, [session]);
@@ -235,7 +247,7 @@ export const AccountSetting: FC = () => {
</Button> </Button>
</SettingRow> </SettingRow>
<StoragePanel /> <StoragePanel />
<AIUsagePanel /> {serverFeatures?.copilot && <AIUsagePanel />}
<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']()}
@@ -86,9 +86,6 @@ export class Subscription extends Entity {
return undefined; // no subscription if no user return undefined; // no subscription if no user
} }
// ensure server config is loaded
this.serverConfigService.serverConfig.revalidateIfNeeded();
const serverConfig = const serverConfig =
await this.serverConfigService.serverConfig.features$.waitForNonNull( await this.serverConfigService.serverConfig.features$.waitForNonNull(
signal signal
@@ -0,0 +1,100 @@
import {
backoffRetry,
catchErrorInto,
effect,
Entity,
exhaustMapSwitchUntilChanged,
fromPromise,
LiveData,
onComplete,
onStart,
} from '@toeverything/infra';
import { EMPTY, map, mergeMap } from 'rxjs';
import { isBackendError, isNetworkError } from '../error';
import type { AuthService } from '../services/auth';
import type { ServerConfigService } from '../services/server-config';
import type { UserCopilotQuotaStore } from '../stores/user-copilot-quota';
export class UserCopilotQuota extends Entity {
copilotActionLimit$ = new LiveData<number | 'unlimited' | null>(null);
copilotActionUsed$ = new LiveData<number | null>(null);
isRevalidating$ = new LiveData(false);
error$ = new LiveData<any | null>(null);
constructor(
private readonly authService: AuthService,
private readonly store: UserCopilotQuotaStore,
private readonly serverConfigService: ServerConfigService
) {
super();
}
revalidate = effect(
map(() => ({
accountId: this.authService.session.account$.value?.id,
})),
exhaustMapSwitchUntilChanged(
(a, b) => a.accountId === b.accountId,
({ accountId }) =>
fromPromise(async signal => {
if (!accountId) {
return; // no quota if no user
}
const serverConfig =
await this.serverConfigService.serverConfig.features$.waitForNonNull(
signal
);
let aiQuota = null;
if (serverConfig.copilot) {
aiQuota = await this.store.fetchUserCopilotQuota(signal);
}
return aiQuota;
}).pipe(
backoffRetry({
when: isNetworkError,
count: Infinity,
}),
backoffRetry({
when: isBackendError,
}),
mergeMap(data => {
if (data) {
const { limit, used } = data;
this.copilotActionUsed$.next(used);
this.copilotActionLimit$.next(
limit === null ? 'unlimited' : limit
); // fix me: unlimited status
} else {
this.copilotActionUsed$.next(null);
this.copilotActionLimit$.next(null);
}
return EMPTY;
}),
catchErrorInto(this.error$),
onStart(() => this.isRevalidating$.next(true)),
onComplete(() => this.isRevalidating$.next(false))
),
() => {
// Reset the state when the user is changed
this.reset();
}
)
);
reset() {
this.copilotActionUsed$.next(null);
this.copilotActionLimit$.next(null);
this.error$.next(null);
this.isRevalidating$.next(false);
}
override dispose(): void {
this.revalidate.unsubscribe();
}
}
@@ -31,9 +31,6 @@ export class UserQuota extends Entity {
/** Maximum storage limit formatted */ /** Maximum storage limit formatted */
maxFormatted$ = this.max$.map(max => (max ? bytes.format(max) : null)); maxFormatted$ = this.max$.map(max => (max ? bytes.format(max) : null));
aiActionLimit$ = new LiveData<number | 'unlimited' | null>(null);
aiActionUsed$ = new LiveData<number | null>(null);
/** Percentage of storage used */ /** Percentage of storage used */
percent$ = LiveData.computed(get => { percent$ = LiveData.computed(get => {
const max = get(this.max$); const max = get(this.max$);
@@ -76,10 +73,9 @@ export class UserQuota extends Entity {
if (!accountId) { if (!accountId) {
return; // no quota if no user return; // no quota if no user
} }
const { quota, aiQuota, used } = const { quota, used } = await this.store.fetchUserQuota(signal);
await this.store.fetchUserQuota(signal);
return { quota, aiQuota, used }; return { quota, used };
}).pipe( }).pipe(
backoffRetry({ backoffRetry({
when: isNetworkError, when: isNetworkError,
@@ -90,18 +86,12 @@ export class UserQuota extends Entity {
}), }),
mergeMap(data => { mergeMap(data => {
if (data) { if (data) {
const { aiQuota, quota, used } = data; const { quota, used } = data;
this.quota$.next(quota); this.quota$.next(quota);
this.used$.next(used); this.used$.next(used);
this.aiActionUsed$.next(aiQuota.used);
this.aiActionLimit$.next(
aiQuota.limit === null ? 'unlimited' : aiQuota.limit
); // fix me: unlimited status
} else { } else {
this.quota$.next(null); this.quota$.next(null);
this.used$.next(null); this.used$.next(null);
this.aiActionUsed$.next(null);
this.aiActionLimit$.next(null);
} }
return EMPTY; return EMPTY;
}), }),
@@ -119,8 +109,6 @@ export class UserQuota extends Entity {
reset() { reset() {
this.quota$.next(null); this.quota$.next(null);
this.used$.next(null); this.used$.next(null);
this.aiActionUsed$.next(null);
this.aiActionLimit$.next(null);
this.error$.next(null); this.error$.next(null);
this.isRevalidating$.next(false); this.isRevalidating$.next(false);
} }
@@ -10,6 +10,7 @@ export { FetchService } from './services/fetch';
export { GraphQLService } from './services/graphql'; export { GraphQLService } from './services/graphql';
export { ServerConfigService } from './services/server-config'; export { ServerConfigService } from './services/server-config';
export { SubscriptionService } from './services/subscription'; export { SubscriptionService } from './services/subscription';
export { UserCopilotQuotaService } from './services/user-copilot-quota';
export { UserFeatureService } from './services/user-feature'; export { UserFeatureService } from './services/user-feature';
export { UserQuotaService } from './services/user-quota'; export { UserQuotaService } from './services/user-quota';
export { WebSocketService } from './services/websocket'; export { WebSocketService } from './services/websocket';
@@ -24,6 +25,7 @@ import { ServerConfig } from './entities/server-config';
import { AuthSession } from './entities/session'; import { AuthSession } from './entities/session';
import { Subscription } from './entities/subscription'; import { Subscription } from './entities/subscription';
import { SubscriptionPrices } from './entities/subscription-prices'; import { SubscriptionPrices } from './entities/subscription-prices';
import { UserCopilotQuota } from './entities/user-copilot-quota';
import { UserFeature } from './entities/user-feature'; import { UserFeature } from './entities/user-feature';
import { UserQuota } from './entities/user-quota'; import { UserQuota } from './entities/user-quota';
import { AuthService } from './services/auth'; import { AuthService } from './services/auth';
@@ -31,12 +33,14 @@ import { FetchService } from './services/fetch';
import { GraphQLService } from './services/graphql'; import { GraphQLService } from './services/graphql';
import { ServerConfigService } from './services/server-config'; import { ServerConfigService } from './services/server-config';
import { SubscriptionService } from './services/subscription'; import { SubscriptionService } from './services/subscription';
import { UserCopilotQuotaService } from './services/user-copilot-quota';
import { UserFeatureService } from './services/user-feature'; import { UserFeatureService } from './services/user-feature';
import { UserQuotaService } from './services/user-quota'; import { UserQuotaService } from './services/user-quota';
import { WebSocketService } from './services/websocket'; import { WebSocketService } from './services/websocket';
import { AuthStore } from './stores/auth'; import { AuthStore } from './stores/auth';
import { ServerConfigStore } from './stores/server-config'; import { ServerConfigStore } from './stores/server-config';
import { SubscriptionStore } from './stores/subscription'; import { SubscriptionStore } from './stores/subscription';
import { UserCopilotQuotaStore } from './stores/user-copilot-quota';
import { UserFeatureStore } from './stores/user-feature'; import { UserFeatureStore } from './stores/user-feature';
import { UserQuotaStore } from './stores/user-quota'; import { UserQuotaStore } from './stores/user-quota';
@@ -58,6 +62,13 @@ export function configureCloudModule(framework: Framework) {
.service(UserQuotaService) .service(UserQuotaService)
.store(UserQuotaStore, [GraphQLService]) .store(UserQuotaStore, [GraphQLService])
.entity(UserQuota, [AuthService, UserQuotaStore]) .entity(UserQuota, [AuthService, UserQuotaStore])
.service(UserCopilotQuotaService)
.store(UserCopilotQuotaStore, [GraphQLService])
.entity(UserCopilotQuota, [
AuthService,
UserCopilotQuotaStore,
ServerConfigService,
])
.service(UserFeatureService) .service(UserFeatureService)
.entity(UserFeature, [AuthService, UserFeatureStore]) .entity(UserFeature, [AuthService, UserFeatureStore])
.store(UserFeatureStore, [GraphQLService]); .store(UserFeatureStore, [GraphQLService]);
@@ -0,0 +1,13 @@
import { OnEvent, Service } from '@toeverything/infra';
import { UserCopilotQuota } from '../entities/user-copilot-quota';
import { AccountChanged } from './auth';
@OnEvent(AccountChanged, e => e.onAccountChanged)
export class UserCopilotQuotaService extends Service {
copilotQuota = this.framework.createEntity(UserCopilotQuota);
private onAccountChanged() {
this.copilotQuota.revalidate();
}
}
@@ -0,0 +1,25 @@
import { copilotQuotaQuery } from '@affine/graphql';
import { Store } from '@toeverything/infra';
import type { GraphQLService } from '../services/graphql';
export class UserCopilotQuotaStore extends Store {
constructor(private readonly graphqlService: GraphQLService) {
super();
}
async fetchUserCopilotQuota(abortSignal?: AbortSignal) {
const data = await this.graphqlService.gql({
query: copilotQuotaQuery,
context: {
signal: abortSignal,
},
});
if (!data.currentUser) {
throw new Error('No logged in');
}
return data.currentUser.copilot.quota;
}
}
@@ -22,7 +22,6 @@ export class UserQuotaStore extends Store {
return { return {
userId: data.currentUser.id, userId: data.currentUser.id,
aiQuota: data.currentUser.copilot.quota,
quota: data.currentUser.quota, quota: data.currentUser.quota,
used: data.collectAllBlobSizes.size, used: data.collectAllBlobSizes.size,
}; };
@@ -1,4 +1,4 @@
query getCopilotQuota { query copilotQuota {
currentUser { currentUser {
copilot { copilot {
quota { quota {
+18 -24
View File
@@ -94,6 +94,24 @@ mutation changePassword($token: String!, $newPassword: String!) {
}`, }`,
}; };
export const copilotQuotaQuery = {
id: 'copilotQuotaQuery' as const,
operationName: 'copilotQuota',
definitionName: 'currentUser',
containsFile: false,
query: `
query copilotQuota {
currentUser {
copilot {
quota {
limit
used
}
}
}
}`,
};
export const createCheckoutSessionMutation = { export const createCheckoutSessionMutation = {
id: 'createCheckoutSessionMutation' as const, id: 'createCheckoutSessionMutation' as const,
operationName: 'createCheckoutSession', operationName: 'createCheckoutSession',
@@ -238,24 +256,6 @@ query getCopilotHistories($workspaceId: String!, $docId: String, $options: Query
}`, }`,
}; };
export const getCopilotQuotaQuery = {
id: 'getCopilotQuotaQuery' as const,
operationName: 'getCopilotQuota',
definitionName: 'currentUser',
containsFile: false,
query: `
query getCopilotQuota {
currentUser {
copilot {
quota {
limit
used
}
}
}
}`,
};
export const getCopilotSessionsQuery = { export const getCopilotSessionsQuery = {
id: 'getCopilotSessionsQuery' as const, id: 'getCopilotSessionsQuery' as const,
operationName: 'getCopilotSessions', operationName: 'getCopilotSessions',
@@ -607,12 +607,6 @@ export const quotaQuery = {
query quota { query quota {
currentUser { currentUser {
id id
copilot {
quota {
limit
used
}
}
quota { quota {
name name
blobLimit blobLimit
@@ -1,12 +1,6 @@
query quota { query quota {
currentUser { currentUser {
id id
copilot {
quota {
limit
used
}
}
quota { quota {
name name
blobLimit blobLimit
+22 -30
View File
@@ -213,6 +213,23 @@ export type ChangePasswordMutation = {
changePassword: { __typename?: 'UserType'; id: string }; changePassword: { __typename?: 'UserType'; id: string };
}; };
export type CopilotQuotaQueryVariables = Exact<{ [key: string]: never }>;
export type CopilotQuotaQuery = {
__typename?: 'Query';
currentUser: {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
quota: {
__typename?: 'CopilotQuota';
limit: number | null;
used: number;
};
};
} | null;
};
export type CreateCheckoutSessionMutationVariables = Exact<{ export type CreateCheckoutSessionMutationVariables = Exact<{
input: CreateCheckoutSessionInput; input: CreateCheckoutSessionInput;
}>; }>;
@@ -353,23 +370,6 @@ export type GetCopilotHistoriesQuery = {
} | null; } | null;
}; };
export type GetCopilotQuotaQueryVariables = Exact<{ [key: string]: never }>;
export type GetCopilotQuotaQuery = {
__typename?: 'Query';
currentUser: {
__typename?: 'UserType';
copilot: {
__typename?: 'Copilot';
quota: {
__typename?: 'CopilotQuota';
limit: number | null;
used: number;
};
};
} | null;
};
export type GetCopilotSessionsQueryVariables = Exact<{ export type GetCopilotSessionsQueryVariables = Exact<{
workspaceId: Scalars['String']['input']; workspaceId: Scalars['String']['input'];
}>; }>;
@@ -677,14 +677,6 @@ export type QuotaQuery = {
currentUser: { currentUser: {
__typename?: 'UserType'; __typename?: 'UserType';
id: string; id: string;
copilot: {
__typename?: 'Copilot';
quota: {
__typename?: 'CopilotQuota';
limit: number | null;
used: number;
};
};
quota: { quota: {
__typename?: 'UserQuota'; __typename?: 'UserQuota';
name: string; name: string;
@@ -1038,6 +1030,11 @@ export type Queries =
variables: ListBlobsQueryVariables; variables: ListBlobsQueryVariables;
response: ListBlobsQuery; response: ListBlobsQuery;
} }
| {
name: 'copilotQuotaQuery';
variables: CopilotQuotaQueryVariables;
response: CopilotQuotaQuery;
}
| { | {
name: 'earlyAccessUsersQuery'; name: 'earlyAccessUsersQuery';
variables: EarlyAccessUsersQueryVariables; variables: EarlyAccessUsersQueryVariables;
@@ -1048,11 +1045,6 @@ export type Queries =
variables: GetCopilotHistoriesQueryVariables; variables: GetCopilotHistoriesQueryVariables;
response: GetCopilotHistoriesQuery; response: GetCopilotHistoriesQuery;
} }
| {
name: 'getCopilotQuotaQuery';
variables: GetCopilotQuotaQueryVariables;
response: GetCopilotQuotaQuery;
}
| { | {
name: 'getCopilotSessionsQuery'; name: 'getCopilotSessionsQuery';
variables: GetCopilotSessionsQueryVariables; variables: GetCopilotSessionsQueryVariables;