refactor(core): add request time out error for ai (#11244)

### Why make this change?
Seperate front end timeout errors from server side errors.

### What changed?
- Add `RequestTimeoutError` which extends from `BaseAIError`.
- Track as `request timeout` instead of `server error`.
This commit is contained in:
akumatus
2025-03-29 04:27:39 +00:00
parent ee66545ac9
commit ac815142b3
21 changed files with 91 additions and 76 deletions

View File

@@ -10,8 +10,8 @@ import {
buildFinishConfig,
buildGeneratingConfig,
} from '../ai-panel';
import type { AIError, AIItemGroupConfig } from '../components/ai-item/types';
import { AIProvider } from '../provider';
import { type AIItemGroupConfig } from '../components/ai-item/types';
import { type AIError, AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { getAIPanelWidget } from '../utils/ai-widgets';
import { AIContext } from '../utils/context';

View File

@@ -20,8 +20,7 @@ import type { TemplateResult } from 'lit';
import { getContentFromSlice } from '../../utils';
import { AIChatBlockModel } from '../blocks';
import type { AIError } from '../components/ai-item/types';
import { AIProvider } from '../provider';
import { type AIError, AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { getAIPanelWidget } from '../utils/ai-widgets';
import { AIContext } from '../utils/context';

View File

@@ -1,6 +1,6 @@
import type { Signal } from '@preact/signals-core';
import type { AIError } from '../components/ai-item/types';
import type { AIError } from '../provider';
export type ChatMessage = {
id: string;

View File

@@ -43,6 +43,7 @@ export class ChatPanelChips extends SignalWatcher(
.chips-wrapper {
display: flex;
flex-wrap: wrap;
margin: 0 -4px 0 -4px;
}
.add-button,
.collapse-button,

View File

@@ -14,8 +14,7 @@ import { property, query, state } from 'lit/decorators.js';
import { repeat } from 'lit/directives/repeat.js';
import { ChatAbortIcon, ChatSendIcon } from '../_common/icons';
import type { AIError } from '../components/ai-item/types';
import { AIProvider } from '../provider';
import { type AIError, AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { readBlobAsURL } from '../utils/image';
import type { AINetworkSearchConfig, DocDisplayConfig } from './chat-config';

View File

@@ -14,8 +14,7 @@ import { repeat } from 'lit/directives/repeat.js';
import { debounce } from 'lodash-es';
import { AffineIcon } from '../_common/icons';
import { type AIError, UnauthorizedError } from '../components/ai-item/types';
import { AIProvider } from '../provider';
import { type AIError, AIProvider, UnauthorizedError } from '../provider';
import {
type ChatContextValue,
type ChatMessage,

View File

@@ -12,8 +12,8 @@ import {
EdgelessEditorActions,
PageEditorActions,
} from '../../_common/chat-actions-handle';
import { type AIError } from '../../components/ai-item/types';
import { AIChatErrorRenderer } from '../../messages/error';
import { type AIError } from '../../provider';
import { type ChatMessage, isChatMessage } from '../chat-context';
export class ChatMessageAssistant extends WithDisposable(ShadowlessElement) {

View File

@@ -28,44 +28,3 @@ export interface AISubItemConfig {
testId?: string;
handler?: (host: EditorHost) => void;
}
abstract class BaseAIError extends Error {
abstract readonly type: AIErrorType;
}
export enum AIErrorType {
GeneralNetworkError = 'GeneralNetworkError',
PaymentRequired = 'PaymentRequired',
Unauthorized = 'Unauthorized',
}
export class UnauthorizedError extends BaseAIError {
readonly type = AIErrorType.Unauthorized;
constructor() {
super('Unauthorized');
}
}
// user has used up the quota
export class PaymentRequiredError extends BaseAIError {
readonly type = AIErrorType.PaymentRequired;
constructor() {
super('Payment required');
}
}
// general 500x error
export class GeneralNetworkError extends BaseAIError {
readonly type = AIErrorType.GeneralNetworkError;
constructor(message: string = 'Network error') {
super(message);
}
}
export type AIError =
| UnauthorizedError
| PaymentRequiredError
| GeneralNetworkError;

View File

@@ -10,10 +10,10 @@ import { property } from 'lit/decorators.js';
import {
type AIError,
AIProvider,
PaymentRequiredError,
UnauthorizedError,
} from '../components/ai-item/types';
import { AIProvider } from '../provider';
} from '../provider';
export class AIErrorWrapper extends SignalWatcher(WithDisposable(LitElement)) {
static override styles = css`

View File

@@ -14,8 +14,7 @@ import {
PROMPT_NAME_AFFINE_AI,
PROMPT_NAME_NETWORK_SEARCH,
} from '../chat-panel/const';
import type { AIError } from '../components/ai-item/types';
import { AIProvider } from '../provider';
import { type AIError, AIProvider } from '../provider';
import { reportResponse } from '../utils/action-reporter';
import { readBlobAsURL } from '../utils/image';
import { stopPropagation } from '../utils/selection-utils';

View File

@@ -28,10 +28,9 @@ import {
ChatMessagesSchema,
} from '../blocks';
import type { AINetworkSearchConfig } from '../chat-panel/chat-config';
import type { AIError } from '../components/ai-item/types';
import type { TextRendererOptions } from '../components/text-renderer';
import { AIChatErrorRenderer } from '../messages/error';
import { AIProvider } from '../provider';
import { type AIError, AIProvider } from '../provider';
import { PeekViewStyles } from './styles';
import type { ChatContext } from './types';
import { calcChildBound } from './utils';

View File

@@ -1,5 +1,5 @@
import type { ChatMessage } from '../blocks';
import type { AIError } from '../components/ai-item/types';
import type { AIError } from '../provider';
export type ChatStatus =
| 'success'

View File

@@ -5,8 +5,9 @@ import { Subject } from 'rxjs';
import type { ChatContextValue } from '../chat-panel/chat-context';
import {
PaymentRequiredError,
RequestTimeoutError,
UnauthorizedError,
} from '../components/ai-item/types';
} from './error';
export interface AIUserInfo {
id: string;
@@ -36,6 +37,7 @@ export type ActionEventType =
| 'aborted:login-required'
| 'aborted:server-error'
| 'aborted:stop'
| 'aborted:timeout'
| 'result:insert'
| 'result:replace'
| 'result:use-as-caption'
@@ -199,7 +201,13 @@ export class AIProvider {
options,
event: 'error',
});
if (err instanceof PaymentRequiredError) {
if (err instanceof RequestTimeoutError) {
slots.actions.next({
action: id,
options,
event: 'aborted:timeout',
});
} else if (err instanceof PaymentRequiredError) {
slots.actions.next({
action: id,
options,

View File

@@ -1,8 +1,3 @@
import {
GeneralNetworkError,
PaymentRequiredError,
UnauthorizedError,
} from '@affine/core/blocksuite/ai/components/ai-item/types';
import { showAILoginRequiredAtom } from '@affine/core/components/affine/auth/ai-login-required';
import type { UserFriendlyError } from '@affine/error';
import {
@@ -31,6 +26,12 @@ import {
} from '@affine/graphql';
import { getCurrentStore } from '@toeverything/infra';
import {
GeneralNetworkError,
PaymentRequiredError,
UnauthorizedError,
} from './error';
type OptionsField<T extends GraphQLQuery> =
RequestOptions<T>['variables'] extends { options: infer U } ? U : never;

View File

@@ -0,0 +1,51 @@
abstract class BaseAIError extends Error {
abstract readonly type: AIErrorType;
}
export enum AIErrorType {
GeneralNetworkError = 'GeneralNetworkError',
PaymentRequired = 'PaymentRequired',
Unauthorized = 'Unauthorized',
RequestTimeout = 'RequestTimeout',
}
export class UnauthorizedError extends BaseAIError {
readonly type = AIErrorType.Unauthorized;
constructor() {
super('Unauthorized');
}
}
// user has used up the quota
export class PaymentRequiredError extends BaseAIError {
readonly type = AIErrorType.PaymentRequired;
constructor() {
super('Payment required');
}
}
// general 500x error
export class GeneralNetworkError extends BaseAIError {
readonly type = AIErrorType.GeneralNetworkError;
constructor(message: string = 'Network error') {
super(message);
}
}
// request timeout
export class RequestTimeoutError extends BaseAIError {
readonly type = AIErrorType.RequestTimeout;
constructor(message: string = 'Request timeout') {
super(message);
}
}
export type AIError =
| UnauthorizedError
| PaymentRequiredError
| GeneralNetworkError
| RequestTimeoutError;

View File

@@ -1,4 +1,5 @@
import { handleError } from './copilot-client';
import { RequestTimeoutError } from './error';
export function delay(ms: number) {
return new Promise(resolve => setTimeout(resolve, ms));
@@ -85,7 +86,7 @@ export function toTextStream(
messagePromise,
delay(timeout).then(() => {
if (!signal?.aborted) {
throw new Error('Timeout');
throw new RequestTimeoutError();
}
}),
])

View File

@@ -1,3 +1,4 @@
export * from './ai-provider';
export * from './copilot-client';
export * from './error';
export * from './setup-provider';

View File

@@ -39,6 +39,7 @@ type AIActionEventProperties = {
| 'policy wall'
| 'server error'
| 'login required'
| 'request timeout'
| 'insert'
| 'replace'
| 'use as caption'
@@ -193,6 +194,8 @@ function inferControl(
return 'server error';
} else if (event.event === 'aborted:login-required') {
return 'login required';
} else if (event.event === 'aborted:timeout') {
return 'request timeout';
} else if (event.options.control === 'chat-send') {
return 'AI chat send button';
} else if (event.options.control === 'block-action-bar') {

View File

@@ -31,10 +31,9 @@ import { property, query } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import { literal, unsafeStatic } from 'lit/static-html.js';
import type { AIError } from '../../components/ai-item/types.js';
import { type AIError } from '../../provider';
import type { AIPanelGenerating } from './components/index.js';
import type { AffineAIPanelState, AffineAIPanelWidgetConfig } from './type.js';
export const AFFINE_AI_PANEL_WIDGET = 'affine-ai-panel-widget';
export class AffineAIPanelWidget extends WidgetComponent {

View File

@@ -5,10 +5,8 @@ import { css, html, LitElement, nothing, unsafeCSS } from 'lit';
import { property } from 'lit/decorators.js';
import { choose } from 'lit/directives/choose.js';
import {
AIErrorType,
type AIItemGroupConfig,
} from '../../../../components/ai-item/types.js';
import { type AIItemGroupConfig } from '../../../../components/ai-item/types.js';
import { AIErrorType } from '../../../../provider';
import type { AIPanelErrorConfig, CopyConfig } from '../../type.js';
import { filterAIItemGroup } from '../../utils.js';

View File

@@ -1,10 +1,8 @@
import type { Signal } from '@preact/signals-core';
import type { nothing, TemplateResult } from 'lit';
import type {
AIError,
AIItemGroupConfig,
} from '../../components/ai-item/types';
import type { AIItemGroupConfig } from '../../components/ai-item/types';
import type { AIError } from '../../provider';
export interface CopyConfig {
allowed: boolean;