fix: detect low balance and prompt for top up (#7166)
Signed-off-by: raj-subhankar <subhankar.rj@gmail.com> Co-authored-by: Douwe Osinga <douwe@squareup.com> Co-authored-by: raj-subhankar <subhankar.rj@gmail.com>
This commit is contained in:
@@ -1167,7 +1167,7 @@ export type SystemNotificationContent = {
|
||||
notificationType: SystemNotificationType;
|
||||
};
|
||||
|
||||
export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage';
|
||||
export type SystemNotificationType = 'thinkingMessage' | 'inlineMessage' | 'creditsExhausted';
|
||||
|
||||
export type TaskSupport = string;
|
||||
|
||||
|
||||
@@ -15,10 +15,17 @@
|
||||
*/
|
||||
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { Message } from '../api';
|
||||
import { Message, SystemNotificationContent } from '../api';
|
||||
import GooseMessage from './GooseMessage';
|
||||
import UserMessage from './UserMessage';
|
||||
import { SystemNotificationInline } from './context_management/SystemNotificationInline';
|
||||
import {
|
||||
SystemNotificationInline,
|
||||
getInlineSystemNotification,
|
||||
} from './context_management/SystemNotificationInline';
|
||||
import {
|
||||
CreditsExhaustedNotification,
|
||||
getCreditsExhaustedNotification,
|
||||
} from './context_management/CreditsExhaustedNotification';
|
||||
import { NotificationEvent } from '../types/message';
|
||||
import LoadingGoose from './LoadingGoose';
|
||||
import { ChatType } from '../types/chat';
|
||||
@@ -71,11 +78,19 @@ export default function ProgressiveMessageList({
|
||||
const hasOnlyToolResponses = (message: Message) =>
|
||||
message.content.every((c) => c.type === 'toolResponse');
|
||||
|
||||
const hasInlineSystemNotification = (message: Message): boolean => {
|
||||
return message.content.some(
|
||||
(content) =>
|
||||
content.type === 'systemNotification' && content.notificationType === 'inlineMessage'
|
||||
);
|
||||
const getSystemNotification = (message: Message): SystemNotificationContent | undefined => {
|
||||
return getCreditsExhaustedNotification(message) ?? getInlineSystemNotification(message);
|
||||
};
|
||||
|
||||
const renderSystemNotification = (notification: SystemNotificationContent) => {
|
||||
switch (notification.notificationType) {
|
||||
case 'creditsExhausted':
|
||||
return <CreditsExhaustedNotification notification={notification} />;
|
||||
case 'inlineMessage':
|
||||
return <SystemNotificationInline notification={notification} />;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Simple progressive loading - start immediately when component mounts if needed
|
||||
@@ -188,15 +203,15 @@ export default function ProgressiveMessageList({
|
||||
return null;
|
||||
}
|
||||
|
||||
// System notifications are never user messages, handle them first
|
||||
if (hasInlineSystemNotification(message)) {
|
||||
const notification = getSystemNotification(message);
|
||||
if (notification) {
|
||||
return (
|
||||
<div
|
||||
key={message.id ?? `msg-${index}-${message.created}`}
|
||||
key={`notification-${message.id ?? `msg-${index}-${message.created}`}`}
|
||||
className={`relative ${index === 0 ? 'mt-0' : 'mt-4'} assistant`}
|
||||
data-testid="message-container"
|
||||
>
|
||||
<SystemNotificationInline message={message} />
|
||||
{renderSystemNotification(notification)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import React from 'react';
|
||||
import { AlertTriangle, ExternalLink } from 'lucide-react';
|
||||
import { Message, SystemNotificationContent } from '../../api';
|
||||
import { WEB_PROTOCOLS } from '../../utils/urlSecurity';
|
||||
|
||||
interface CreditsExhaustedNotificationProps {
|
||||
notification: SystemNotificationContent;
|
||||
}
|
||||
|
||||
function getValidatedTopUpUrl(data: unknown): string | null {
|
||||
if (!data || typeof data !== 'object') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const rawUrl = (data as Record<string, unknown>).top_up_url;
|
||||
if (typeof rawUrl !== 'string') {
|
||||
return null;
|
||||
}
|
||||
|
||||
const url = rawUrl.trim();
|
||||
if (!url) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsedUrl = new URL(url);
|
||||
if (!WEB_PROTOCOLS.includes(parsedUrl.protocol)) {
|
||||
return null;
|
||||
}
|
||||
return parsedUrl.toString();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export const CreditsExhaustedNotification: React.FC<CreditsExhaustedNotificationProps> = ({
|
||||
notification,
|
||||
}) => {
|
||||
const topUpUrl = getValidatedTopUpUrl(notification.data);
|
||||
|
||||
const handleTopUp = () => {
|
||||
if (topUpUrl) {
|
||||
window.electron.openExternal(topUpUrl);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-yellow-600/30 dark:border-yellow-500/30 bg-yellow-500/10 dark:bg-yellow-500/10 p-4 my-2">
|
||||
<div className="flex items-start gap-3">
|
||||
<AlertTriangle className="h-4 w-4 text-yellow-600 dark:text-yellow-400 mt-0.5 shrink-0" />
|
||||
<div className="flex-1">
|
||||
<div className="text-sm font-semibold text-yellow-800 dark:text-yellow-200">Insufficient Credits</div>
|
||||
<div className="text-sm text-yellow-800/80 dark:text-yellow-200/80 mt-1">{notification.msg}</div>
|
||||
{topUpUrl && (
|
||||
<button
|
||||
onClick={handleTopUp}
|
||||
className="mt-3 inline-flex items-center gap-2 rounded-md bg-yellow-600 hover:bg-yellow-500 dark:bg-yellow-700 dark:hover:bg-yellow-600 text-white text-sm font-medium px-4 py-2 transition-colors"
|
||||
>
|
||||
Add credits
|
||||
<ExternalLink className="h-3.5 w-3.5" />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export function getCreditsExhaustedNotification(
|
||||
message: Message
|
||||
): SystemNotificationContent | undefined {
|
||||
return message.content.find(
|
||||
(content): content is SystemNotificationContent & { type: 'systemNotification' } =>
|
||||
content.type === 'systemNotification' && content.notificationType === 'creditsExhausted'
|
||||
);
|
||||
}
|
||||
@@ -2,18 +2,20 @@ import React from 'react';
|
||||
import { Message, SystemNotificationContent } from '../../api';
|
||||
|
||||
interface SystemNotificationInlineProps {
|
||||
message: Message;
|
||||
notification: SystemNotificationContent;
|
||||
}
|
||||
|
||||
export const SystemNotificationInline: React.FC<SystemNotificationInlineProps> = ({ message }) => {
|
||||
const systemNotification = message.content.find(
|
||||
export const SystemNotificationInline: React.FC<SystemNotificationInlineProps> = ({
|
||||
notification,
|
||||
}) => {
|
||||
return <div className="text-xs text-gray-400 py-2 text-left">{notification.msg}</div>;
|
||||
};
|
||||
|
||||
export function getInlineSystemNotification(
|
||||
message: Message
|
||||
): SystemNotificationContent | undefined {
|
||||
return message.content.find(
|
||||
(content): content is SystemNotificationContent & { type: 'systemNotification' } =>
|
||||
content.type === 'systemNotification' && content.notificationType === 'inlineMessage'
|
||||
);
|
||||
|
||||
if (!systemNotification?.msg) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return <div className="text-xs text-gray-400 py-2 text-left">{systemNotification.msg}</div>;
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user