From b46e9d2e646809d0fe6a3efe53b651391f29b72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?F=C3=A9lix=20Malfait?= Date: Wed, 24 Dec 2025 15:30:28 +0100 Subject: [PATCH] feat: add AI chat error handling for billing and API key errors (#16797) ## Summary This PR adds user-friendly error handling for AI chat features, specifically for **billing credits exhausted** and **API key not configured** errors. ## Changes ### Backend - Added `BILLING_CREDITS_EXHAUSTED` exception code with 402 status - Added `API_KEY_NOT_CONFIGURED` exception code with 503 status - Added billing check before AI chat streaming in `agent-chat.controller.ts` - Added error code to HTTP exception response body for frontend error type detection - Created `AgentRestApiExceptionFilter` for agent-specific errors ### Frontend - Created `AIChatBanner` - reusable banner component for error/warning messages - Created `AIChatCreditsExhaustedMessage` - shows upgrade prompts based on user permissions - Created `AIChatApiKeyNotConfiguredMessage` - shows configuration guidance with docs link - Created `AIChatErrorRenderer` - encapsulates error type switching logic (fixes nested ternary) - Created `AIChatStandaloneError` - displays errors when there are no messages - Split `aiChatErrorUtils.ts` into separate files (1 export per file): - `AIChatErrorCode.ts` - `extractErrorCode.ts` - `isAIChatErrorOfType.ts` - `isBillingCreditsExhaustedError.ts` - `isApiKeyNotConfiguredError.ts` - Added comprehensive test coverage (27 tests) ### Other - Updated trial period banner messaging ## Testing - All lint checks pass - All 27 new tests pass - TypeScript typecheck passes --- .../AIChatApiKeyNotConfiguredMessage.tsx | 22 ++++ .../modules/ai/components/AIChatBanner.tsx | 106 ++++++++++++++++++ .../AIChatCreditsExhaustedMessage.tsx | 84 ++++++++++++++ .../ai/components/AIChatErrorRenderer.tsx | 21 ++++ .../modules/ai/components/AIChatMessage.tsx | 4 +- .../ai/components/AIChatStandaloneError.tsx | 55 +++++++++ .../src/modules/ai/components/AIChatTab.tsx | 16 ++- .../src/modules/ai/hooks/useAgentChat.ts | 22 +++- .../src/modules/ai/utils/AIChatErrorCode.ts | 8 ++ .../utils/__tests__/extractErrorCode.test.ts | 66 +++++++++++ .../__tests__/isAIChatErrorOfType.test.ts | 60 ++++++++++ .../isApiKeyNotConfiguredError.test.ts | 31 +++++ .../isBillingCreditsExhaustedError.test.ts | 31 +++++ .../src/modules/ai/utils/extractErrorCode.ts | 53 +++++++++ .../modules/ai/utils/isAIChatErrorOfType.ts | 15 +++ .../ai/utils/isApiKeyNotConfiguredError.ts | 8 ++ .../utils/isBillingCreditsExhaustedError.ts | 8 ++ .../InformationBannerEndTrialPeriod.tsx | 8 +- .../core-modules/billing/billing.exception.ts | 2 + .../filters/billing-api-exception.filter.ts | 6 + .../billing/services/billing.service.ts | 6 +- .../http-exception-handler.service.ts | 13 ++- .../filters/agent-api-exception.filter.ts | 58 ++++++++++ .../ai/ai-chat/ai-chat.module.ts | 2 + .../controllers/agent-chat.controller.ts | 55 ++++++++- .../constants/ai-models.const.spec.ts | 2 +- .../services/ai-model-registry.service.ts | 40 +++++-- 27 files changed, 767 insertions(+), 35 deletions(-) create mode 100644 packages/twenty-front/src/modules/ai/components/AIChatApiKeyNotConfiguredMessage.tsx create mode 100644 packages/twenty-front/src/modules/ai/components/AIChatBanner.tsx create mode 100644 packages/twenty-front/src/modules/ai/components/AIChatCreditsExhaustedMessage.tsx create mode 100644 packages/twenty-front/src/modules/ai/components/AIChatErrorRenderer.tsx create mode 100644 packages/twenty-front/src/modules/ai/components/AIChatStandaloneError.tsx create mode 100644 packages/twenty-front/src/modules/ai/utils/AIChatErrorCode.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/__tests__/extractErrorCode.test.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/__tests__/isAIChatErrorOfType.test.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/__tests__/isApiKeyNotConfiguredError.test.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/__tests__/isBillingCreditsExhaustedError.test.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/extractErrorCode.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/isAIChatErrorOfType.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/isApiKeyNotConfiguredError.ts create mode 100644 packages/twenty-front/src/modules/ai/utils/isBillingCreditsExhaustedError.ts create mode 100644 packages/twenty-server/src/engine/metadata-modules/ai/ai-agent/filters/agent-api-exception.filter.ts diff --git a/packages/twenty-front/src/modules/ai/components/AIChatApiKeyNotConfiguredMessage.tsx b/packages/twenty-front/src/modules/ai/components/AIChatApiKeyNotConfiguredMessage.tsx new file mode 100644 index 00000000000..df26fc79547 --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/AIChatApiKeyNotConfiguredMessage.tsx @@ -0,0 +1,22 @@ +import { AIChatBanner } from '@/ai/components/AIChatBanner'; +import { t } from '@lingui/core/macro'; +import { IconExternalLink } from 'twenty-ui/display'; + +const DOCS_URL = + 'https://twenty.com/developers/section/self-hosting/self-hosting-var#ai-features'; + +export const AIChatApiKeyNotConfiguredMessage = () => { + const handleDocsClick = () => { + window.open(DOCS_URL, '_blank', 'noopener,noreferrer'); + }; + + return ( + + ); +}; diff --git a/packages/twenty-front/src/modules/ai/components/AIChatBanner.tsx b/packages/twenty-front/src/modules/ai/components/AIChatBanner.tsx new file mode 100644 index 00000000000..dce8ea73e4f --- /dev/null +++ b/packages/twenty-front/src/modules/ai/components/AIChatBanner.tsx @@ -0,0 +1,106 @@ +import styled from '@emotion/styled'; +import { isDefined } from 'twenty-shared/utils'; +import { + AppTooltip, + type IconComponent, + IconAlertTriangle, + IconInfoCircle, +} from 'twenty-ui/display'; +import { Button } from 'twenty-ui/input'; + +type AIChatBannerVariant = 'default' | 'warning'; + +const StyledBanner = styled.div<{ variant: AIChatBannerVariant }>` + align-items: center; + background-color: ${({ theme, variant }) => + variant === 'warning' + ? theme.background.transparent.orange + : theme.accent.secondary}; + border-radius: ${({ theme }) => theme.border.radius.md}; + box-sizing: border-box; + display: flex; + gap: ${({ theme }) => theme.spacing(2)}; + padding: ${({ theme }) => theme.spacing(2)}; + width: 100%; +`; + +const StyledIconContainer = styled.div<{ variant: AIChatBannerVariant }>` + align-items: center; + color: ${({ theme, variant }) => + variant === 'warning' ? theme.color.orange : theme.color.blue}; + display: flex; + flex-shrink: 0; + height: 16px; + justify-content: center; + width: 16px; +`; + +const StyledMessage = styled.p<{ variant: AIChatBannerVariant }>` + color: ${({ theme, variant }) => + variant === 'warning' ? theme.color.orange : theme.color.blue}; + flex-grow: 1; + font-family: ${({ theme }) => theme.font.family}; + font-size: ${({ theme }) => theme.font.size.sm}; + font-style: normal; + font-weight: ${({ theme }) => theme.font.weight.medium}; + line-height: 1.4; + margin: 0; + min-width: 0; +`; + +export type AIChatBannerProps = { + message: string; + variant?: AIChatBannerVariant; + tooltipMessage?: string; + buttonTitle?: string; + buttonIcon?: IconComponent; + buttonOnClick?: () => void; + isButtonDisabled?: boolean; + isButtonLoading?: boolean; +}; + +export const AIChatBanner = ({ + message, + variant = 'default', + tooltipMessage, + buttonTitle, + buttonIcon, + buttonOnClick, + isButtonDisabled = false, + isButtonLoading = false, +}: AIChatBannerProps) => { + const tooltipId = 'ai-chat-banner-tooltip'; + + return ( + + + {variant === 'default' ? ( + + ) : ( + + )} + + {message} + {isDefined(buttonTitle) && isDefined(buttonOnClick) && ( +