Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 24 additions & 8 deletions packages/app/src/react/features/ai-chat/clients/chat-api.client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,6 @@ export class ChatAPIClient {
...headers,
};


const requestBody = { message, attachments, enableMetaMessages: true };

// Make streaming request
Expand Down Expand Up @@ -276,42 +275,59 @@ export class ChatAPIClient {
* @returns Structured chat error
*/
private handleStreamError(error: unknown, signal: AbortSignal): TChatError {
// Check if this is an abort error
if (error instanceof DOMException && error.name === 'AbortError') {
return {
message: signal.aborted ? 'Request was cancelled' : 'Stream was aborted',
type: 'abort',
isAborted: true,
isRetryable: false,
originalError: error,
};
}

// Handle network errors
if (error instanceof TypeError && error.message.includes('fetch')) {
return {
message: `Network request failed: ${error.message}`,
type: 'network',
isRetryable: true,
originalError: error,
};
}

// Handle generic errors
if (error instanceof Error) {
return {
message: error.message || 'An unexpected error occurred',
type: 'system',
isRetryable: true,
originalError: error,
};
}

// Unknown error type
// Plain objects — TChatError thrown from processChunk / HTTP error handlers
const raw = error as TChatError;
const rawMessage = typeof raw?.message === 'string' ? raw.message : '';
const friendly = this.toUserFriendlyMessage(rawMessage);

return {
message: 'An unexpected error occurred. Please try again.',
type: 'system',
originalError: error,
message: friendly.message || 'An unexpected error occurred. Please try again.',
type: raw?.type ?? 'system',
isRetryable: friendly.isRetryable,
originalError: raw?.originalError ?? error,
};
}

private toUserFriendlyMessage(message: string): { message: string; isRetryable: boolean } {
if (/maximum context length/i.test(message)) {
return {
message:
'This conversation has reached its memory limit. Please start a new chat to keep things running smoothly.',
isRetryable: false,
};
}

return { message, isRetryable: true };
}

/**
* Parses error response from HTTP response
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ export const Chat: FC<IProps> = memo((props) => {
case MESSAGE_TYPES.INFO:
return <Info message={content} />;
case MESSAGE_TYPES.ERROR:
return <Error message={content} retry={retry} />;
return <Error message={content} retry={retry} isRetryable={props.isRetryable} />;
default:
return <Error message="Something went wrong!" retry={retry} />;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import { FC } from 'react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';

type TProps = TMessageProps & { retry: () => void };
type TProps = TMessageProps & { retry: () => void; isRetryable?: boolean };

export const Error: FC<TProps> = ({ message, retry }) => {
export const Error: FC<TProps> = ({ message, retry, isRetryable = true }) => {
const isApiKeyError =
message.includes('Incorrect API key provided') ||
(message.includes('401') &&
Expand Down Expand Up @@ -41,7 +41,7 @@ export const Error: FC<TProps> = ({ message, retry }) => {
<ReactMarkdown remarkPlugins={[remarkGfm]}>{message}</ReactMarkdown>
</div>
</div>
{retry && (
{retry && isRetryable && (
<button
onClick={retry}
className="inline-flex items-center px-4 gap-x-1 py-2 border border-gray-300 text-sm font-medium rounded-[18px] text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed transition-colors shadow-sm"
Expand Down
60 changes: 32 additions & 28 deletions packages/app/src/react/features/ai-chat/hooks/use-chat-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,11 +178,14 @@ export const useChatState = (options: TChatStateConfig): IChatState => {
}

setMessages((prev) => {
const filtered = prev.filter((msg) => msg.type !== MESSAGE_TYPES.LOADING);
const filtered = prev.filter(
(msg) => msg.type !== MESSAGE_TYPES.LOADING && msg.type !== MESSAGE_TYPES.META,
);
const errorMessage: TChatMessage = {
id: Date.now() + Math.random(),
type: MESSAGE_TYPES.ERROR,
content: error.message || 'An error occurred',
isRetryable: error.isRetryable,
updatedAt: Date.now(),
};
return [...filtered, errorMessage];
Expand All @@ -209,23 +212,23 @@ export const useChatState = (options: TChatStateConfig): IChatState => {

const userMessage: TChatMessage | null = shouldSetUserMessage
? {
id: msgCount + 1,
type: MESSAGE_TYPES.USER,
content: message?.trim() || '',
attachments:
currentAttachments.length > 0
? currentAttachments.map((a) => ({
id: a.id,
name: a.name,
type: a.type,
size: a.size,
url: a.url,
blobUrl: a.blobUrl,
file: a.file,
}))
: undefined,
updatedAt: now,
}
id: msgCount + 1,
type: MESSAGE_TYPES.USER,
content: message?.trim() || '',
attachments:
currentAttachments.length > 0
? currentAttachments.map((a) => ({
id: a.id,
name: a.name,
type: a.type,
size: a.size,
url: a.url,
blobUrl: a.blobUrl,
file: a.file,
}))
: undefined,
updatedAt: now,
}
: null;

const loadingMessage: TChatMessage = {
Expand All @@ -247,16 +250,16 @@ export const useChatState = (options: TChatStateConfig): IChatState => {
attachments:
currentAttachments.length > 0
? currentAttachments
.filter((a) => a.file)
.map((a) => ({
id: `${Date.now()}_${Math.random()}`,
file: a.file as File,
name: a.name,
type: a.type,
size: a.size,
url: a.url || '',
metadata: {},
}))
.filter((a) => a.file)
.map((a) => ({
id: `${Date.now()}_${Math.random()}`,
file: a.file as File,
name: a.name,
type: a.type,
size: a.size,
url: a.url || '',
metadata: {},
}))
: undefined,
agentId,
chatId,
Expand Down Expand Up @@ -287,6 +290,7 @@ export const useChatState = (options: TChatStateConfig): IChatState => {
);
} catch (error) {
console.error('Error in sendMessage:', error); // eslint-disable-line no-console
// onStreamError(error as TChatError);
} finally {
setIsStreaming(false);
abortRef.current = null;
Expand Down
1 change: 1 addition & 0 deletions packages/app/src/react/features/ai-chat/types/message.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,5 @@ export type TChatMessage = {
metaMessages?: TMetaMessage;
updatedAt?: number;
attachments?: TAttachment[];
isRetryable?: boolean;
};
1 change: 1 addition & 0 deletions packages/app/src/react/features/ai-chat/types/stream.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export type TChatError = {
type: TErrorType;
originalError?: Error | unknown;
isAborted?: boolean;
isRetryable?: boolean;
};

export type TAPIConfig = {
Expand Down