Skip to content

Commit 7129b3f

Browse files
committed
Add limit v1
1 parent f7d2c96 commit 7129b3f

File tree

5 files changed

+171
-2
lines changed

5 files changed

+171
-2
lines changed

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ export * from './file-display'
22
export { default as CopilotMarkdownRenderer } from './markdown-renderer'
33
export * from './smooth-streaming'
44
export * from './thinking-block'
5+
export * from './usage-limit-actions'
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
'use client'
2+
3+
import { useState } from 'react'
4+
import { useParams, useRouter } from 'next/navigation'
5+
import { ArrowRight, Check, CreditCard, Loader2 } from 'lucide-react'
6+
import { Button } from '@/components/emcn'
7+
import { useSubscriptionData, useUpdateUsageLimit } from '@/hooks/queries/subscription'
8+
import { canEditUsageLimit } from '@/lib/billing/subscriptions/utils'
9+
10+
/**
11+
* Component that displays actionable UI when a user hits their usage limit
12+
* Shows inline input to increase limit or button to upgrade plan
13+
*/
14+
export function UsageLimitActions() {
15+
const router = useRouter()
16+
const params = useParams()
17+
const workspaceId = params?.workspaceId as string
18+
const { data: subscriptionData } = useSubscriptionData()
19+
const updateUsageLimitMutation = useUpdateUsageLimit()
20+
21+
const subscription = subscriptionData?.subscription
22+
const canEdit = subscription ? canEditUsageLimit(subscription) : false
23+
const currentLimit = subscriptionData?.data?.usage?.limit || 10
24+
const currentUsage = subscriptionData?.data?.usage?.current || 0
25+
26+
// Suggest an increase of at least $10 or enough to cover current usage + $5
27+
const suggestedIncrease = Math.max(10, Math.ceil(currentUsage - currentLimit + 5))
28+
const suggestedLimit = currentLimit + suggestedIncrease
29+
30+
const [newLimit, setNewLimit] = useState(suggestedLimit.toString())
31+
const [showSuccess, setShowSuccess] = useState(false)
32+
33+
const handleUpdateLimit = async () => {
34+
const limitValue = Number.parseFloat(newLimit)
35+
if (Number.isNaN(limitValue) || limitValue <= currentLimit) {
36+
return
37+
}
38+
39+
try {
40+
await updateUsageLimitMutation.mutateAsync({ limit: limitValue })
41+
setShowSuccess(true)
42+
setTimeout(() => setShowSuccess(false), 3000)
43+
} catch (error) {
44+
// Error is handled by the mutation
45+
}
46+
}
47+
48+
const handleNavigateToUpgrade = () => {
49+
router.push(`/workspace/${workspaceId}/settings?tab=subscription`)
50+
}
51+
52+
if (!canEdit) {
53+
// Show upgrade button for users who can't edit (free/enterprise)
54+
return (
55+
<div className='mt-3 flex flex-col gap-2 rounded-lg border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-950'>
56+
<div className='flex items-start gap-2'>
57+
<CreditCard className='mt-0.5 h-4 w-4 flex-shrink-0 text-orange-600 dark:text-orange-400' />
58+
<div className='flex-1'>
59+
<p className='text-sm font-medium text-orange-900 dark:text-orange-100'>
60+
Usage Limit Reached
61+
</p>
62+
<p className='mt-1 text-xs text-orange-700 dark:text-orange-300'>
63+
Upgrade your plan to get higher limits and continue using the copilot.
64+
</p>
65+
</div>
66+
</div>
67+
<Button
68+
onClick={handleNavigateToUpgrade}
69+
className='w-full justify-between text-sm'
70+
variant='default'
71+
>
72+
<span>Upgrade Plan</span>
73+
<ArrowRight className='h-4 w-4' />
74+
</Button>
75+
</div>
76+
)
77+
}
78+
79+
// Show inline edit for users who can edit their limit
80+
return (
81+
<div className='mt-3 flex flex-col gap-2 rounded-lg border border-orange-200 bg-orange-50 p-3 dark:border-orange-800 dark:bg-orange-950'>
82+
<div className='flex items-start gap-2'>
83+
<CreditCard className='mt-0.5 h-4 w-4 flex-shrink-0 text-orange-600 dark:text-orange-400' />
84+
<div className='flex-1'>
85+
<p className='text-sm font-medium text-orange-900 dark:text-orange-100'>
86+
Usage Limit Reached
87+
</p>
88+
<p className='mt-1 text-xs text-orange-700 dark:text-orange-300'>
89+
Current limit: ${currentLimit}. Increase your limit to continue.
90+
</p>
91+
</div>
92+
</div>
93+
94+
{showSuccess ? (
95+
<div className='flex items-center gap-2 rounded-md bg-green-100 px-3 py-2 dark:bg-green-900'>
96+
<Check className='h-4 w-4 text-green-700 dark:text-green-300' />
97+
<span className='text-sm text-green-800 dark:text-green-200'>
98+
Limit updated successfully!
99+
</span>
100+
</div>
101+
) : (
102+
<div className='flex gap-2'>
103+
<div className='relative flex-1'>
104+
<span className='pointer-events-none absolute left-3 top-1/2 -translate-y-1/2 text-sm text-gray-600 dark:text-gray-400'>
105+
$
106+
</span>
107+
<input
108+
type='number'
109+
value={newLimit}
110+
onChange={(e) => setNewLimit(e.target.value)}
111+
min={currentLimit}
112+
step='1'
113+
className='h-9 w-full rounded-md border border-orange-300 bg-white pl-6 pr-3 text-sm text-gray-900 focus:border-orange-500 focus:outline-none focus:ring-1 focus:ring-orange-500 dark:border-orange-700 dark:bg-gray-800 dark:text-gray-100'
114+
placeholder={suggestedLimit.toString()}
115+
/>
116+
</div>
117+
<Button
118+
onClick={handleUpdateLimit}
119+
disabled={
120+
updateUsageLimitMutation.isPending ||
121+
Number.isNaN(Number.parseFloat(newLimit)) ||
122+
Number.parseFloat(newLimit) <= currentLimit
123+
}
124+
className='h-9 px-4 text-sm'
125+
variant='primary'
126+
>
127+
{updateUsageLimitMutation.isPending ? (
128+
<>
129+
<Loader2 className='mr-2 h-4 w-4 animate-spin' />
130+
Updating...
131+
</>
132+
) : (
133+
'Update Limit'
134+
)}
135+
</Button>
136+
</div>
137+
)}
138+
139+
{updateUsageLimitMutation.isError && (
140+
<p className='text-xs text-red-700 dark:text-red-400'>
141+
{updateUsageLimitMutation.error?.message || 'Failed to update limit'}
142+
</p>
143+
)}
144+
</div>
145+
)
146+
}

apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/copilot-message.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
SmoothStreamingText,
1010
StreamingIndicator,
1111
ThinkingBlock,
12+
UsageLimitActions,
1213
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components'
1314
import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
1415
import {
@@ -458,6 +459,9 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
458459
<StreamingIndicator />
459460
)}
460461

462+
{/* Usage limit actions for 402 errors */}
463+
{message.errorType === 'usage_limit' && <UsageLimitActions />}
464+
461465
{/* Action buttons for completed messages */}
462466
{!isStreaming && cleanTextContent && (
463467
<div className='flex items-center gap-[8px] pt-[8px]'>

apps/sim/stores/panel/copilot/store.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -530,7 +530,11 @@ function createStreamingMessage(): CopilotMessage {
530530
}
531531
}
532532

533-
function createErrorMessage(messageId: string, content: string): CopilotMessage {
533+
function createErrorMessage(
534+
messageId: string,
535+
content: string,
536+
errorType?: 'usage_limit' | 'unauthorized' | 'forbidden' | 'rate_limit' | 'upgrade_required'
537+
): CopilotMessage {
534538
return {
535539
id: messageId,
536540
role: 'assistant',
@@ -543,6 +547,7 @@ function createErrorMessage(messageId: string, content: string): CopilotMessage
543547
timestamp: Date.now(),
544548
},
545549
],
550+
errorType,
546551
}
547552
}
548553

@@ -2063,23 +2068,35 @@ export const useCopilotStore = create<CopilotStore>()(
20632068

20642069
// Check for specific status codes and provide custom messages
20652070
let errorContent = result.error || 'Failed to send message'
2071+
let errorType:
2072+
| 'usage_limit'
2073+
| 'unauthorized'
2074+
| 'forbidden'
2075+
| 'rate_limit'
2076+
| 'upgrade_required'
2077+
| undefined = undefined
20662078
if (result.status === 401) {
20672079
errorContent =
20682080
'_Unauthorized request. You need a valid API key to use the copilot. You can get one by going to [sim.ai](https://sim.ai) settings and generating one there._'
2081+
errorType = 'unauthorized'
20692082
} else if (result.status === 402) {
20702083
errorContent =
20712084
'_Usage limit exceeded. To continue using this service, upgrade your plan or top up on credits._'
2085+
errorType = 'usage_limit'
20722086
} else if (result.status === 403) {
20732087
errorContent =
20742088
'_Provider config not allowed for non-enterprise users. Please remove the provider config and try again_'
2089+
errorType = 'forbidden'
20752090
} else if (result.status === 426) {
20762091
errorContent =
20772092
'_Please upgrade to the latest version of the Sim platform to continue using the copilot._'
2093+
errorType = 'upgrade_required'
20782094
} else if (result.status === 429) {
20792095
errorContent = '_Provider rate limit exceeded. Please try again later._'
2096+
errorType = 'rate_limit'
20802097
}
20812098

2082-
const errorMessage = createErrorMessage(streamingMessage.id, errorContent)
2099+
const errorMessage = createErrorMessage(streamingMessage.id, errorContent, errorType)
20832100
set((state) => ({
20842101
messages: state.messages.map((m) => (m.id === streamingMessage.id ? errorMessage : m)),
20852102
error: errorContent,

apps/sim/stores/panel/copilot/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ export interface CopilotMessage {
3939
>
4040
fileAttachments?: MessageFileAttachment[]
4141
contexts?: ChatContext[]
42+
errorType?: 'usage_limit' | 'unauthorized' | 'forbidden' | 'rate_limit' | 'upgrade_required'
4243
}
4344

4445
// Contexts attached to a user message

0 commit comments

Comments
 (0)