diff --git a/apps/x/apps/renderer/src/App.tsx b/apps/x/apps/renderer/src/App.tsx index e6c050b3..0ca4b6b3 100644 --- a/apps/x/apps/renderer/src/App.tsx +++ b/apps/x/apps/renderer/src/App.tsx @@ -63,6 +63,8 @@ import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/comp import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from "@/components/ui/dialog" import { Button } from "@/components/ui/button" import { Toaster } from "@/components/ui/sonner" +import { BillingErrorDialog } from "@/components/billing-error-dialog" +import { matchBillingError, type BillingErrorMatch } from "@/lib/billing-error" import { stripKnowledgePrefix, toKnowledgePath, wikiLabel } from '@/lib/wiki-links' import { splitFrontmatter, joinFrontmatter } from '@/lib/frontmatter' import { extractConferenceLink } from '@/lib/calendar-event' @@ -793,7 +795,25 @@ function App() { // Chat state const [, setMessage] = useState('') const [conversation, setConversation] = useState([]) + const [billingErrorMatch, setBillingErrorMatch] = useState(null) + const [billingErrorOpen, setBillingErrorOpen] = useState(false) + const lastHandledBillingErrorIdRef = useRef(null) const [currentAssistantMessage, setCurrentAssistantMessage] = useState('') + + useEffect(() => { + for (let i = conversation.length - 1; i >= 0; i--) { + const item = conversation[i] + if (!isErrorMessage(item)) continue + if (item.id === lastHandledBillingErrorIdRef.current) return + const match = matchBillingError(item.message) + if (match) { + lastHandledBillingErrorIdRef.current = item.id + setBillingErrorMatch(match) + setBillingErrorOpen(true) + } + return + } + }, [conversation]) const [, setModelUsage] = useState(null) const [runId, setRunId] = useState(null) const runIdRef = useRef(null) @@ -5399,6 +5419,11 @@ function App() { /> + void +} + +export function BillingErrorDialog({ open, match, onOpenChange }: BillingErrorDialogProps) { + const [appUrl, setAppUrl] = useState(null) + + useEffect(() => { + if (!open) return + window.ipc + .invoke('account:getRowboat', null) + .then((account: BillingRowboatAccount) => setAppUrl(account.config?.appUrl ?? null)) + .catch(() => {}) + }, [open]) + + if (!match) return null + + const handleUpgrade = () => { + if (appUrl) window.open(`${appUrl}?intent=upgrade`) + onOpenChange(false) + } + + return ( + + + + {match.title} + {match.subtitle} + + + + + + + + ) +} diff --git a/apps/x/apps/renderer/src/components/chat-sidebar.tsx b/apps/x/apps/renderer/src/components/chat-sidebar.tsx index 06680652..4ce3e533 100644 --- a/apps/x/apps/renderer/src/components/chat-sidebar.tsx +++ b/apps/x/apps/renderer/src/components/chat-sidebar.tsx @@ -52,6 +52,7 @@ import { parseAttachedFiles, toToolState, } from '@/lib/chat-conversation' +import { matchBillingError } from '@/lib/billing-error' const streamdownComponents = { pre: MarkdownPreOverride } @@ -87,31 +88,6 @@ function AutoScrollPre({ className, children }: { className?: string; children: /* ─── Billing error helpers ─── */ -const BILLING_ERROR_PATTERNS = [ - { - pattern: /upgrade required/i, - title: 'A subscription is required', - subtitle: 'Get started with a plan to access AI features in Rowboat.', - cta: 'Subscribe', - }, - { - pattern: /not enough credits/i, - title: 'You\'ve run out of credits', - subtitle: 'Upgrade your plan for more credits. Free usage resets daily at 00:00 UTC.', - cta: 'Upgrade plan', - }, - { - pattern: /subscription not active/i, - title: 'Your subscription is inactive', - subtitle: 'Reactivate your subscription to continue using AI features.', - cta: 'Reactivate', - }, -] as const - -function matchBillingError(message: string) { - return BILLING_ERROR_PATTERNS.find(({ pattern }) => pattern.test(message)) ?? null -} - interface BillingRowboatAccount { config?: { appUrl?: string | null diff --git a/apps/x/apps/renderer/src/lib/billing-error.ts b/apps/x/apps/renderer/src/lib/billing-error.ts new file mode 100644 index 00000000..ffb77470 --- /dev/null +++ b/apps/x/apps/renderer/src/lib/billing-error.ts @@ -0,0 +1,26 @@ +export const BILLING_ERROR_PATTERNS = [ + { + pattern: /upgrade required/i, + title: 'A subscription is required', + subtitle: 'Get started with a plan to access AI features in Rowboat.', + cta: 'Subscribe', + }, + { + pattern: /not enough credits/i, + title: "You've run out of credits", + subtitle: 'Upgrade your plan for more credits. Free usage resets daily at 00:00 UTC.', + cta: 'Upgrade plan', + }, + { + pattern: /subscription not active/i, + title: 'Your subscription is inactive', + subtitle: 'Reactivate your subscription to continue using AI features.', + cta: 'Reactivate', + }, +] as const + +export type BillingErrorMatch = (typeof BILLING_ERROR_PATTERNS)[number] + +export function matchBillingError(message: string): BillingErrorMatch | null { + return BILLING_ERROR_PATTERNS.find(({ pattern }) => pattern.test(message)) ?? null +}