feat(copilot): show inline prompt to increase usage limit or upgrade plan (#2465)

* Add limit v1

* fix ui for copilot upgrade limit inline

* open settings modal

* Upgrade plan button

* Remove comments

* Ishosted check

* Fix hardcoded bumps

---------

Co-authored-by: Vikhyath Mondreti <vikhyathvikku@gmail.com>
Co-authored-by: Waleed <walif6@gmail.com>
Co-authored-by: Vikhyath Mondreti <vikhyath@simstudio.ai>
This commit is contained in:
Siddharth Ganesan
2025-12-20 13:46:06 -08:00
committed by GitHub
parent 6247f421bc
commit 0ebb45b2db
5 changed files with 128 additions and 3 deletions

View File

@@ -2,3 +2,4 @@ export * from './file-display'
export { default as CopilotMarkdownRenderer } from './markdown-renderer'
export * from './smooth-streaming'
export * from './thinking-block'
export * from './usage-limit-actions'

View File

@@ -0,0 +1,99 @@
'use client'
import { useState } from 'react'
import { Loader2 } from 'lucide-react'
import { Button } from '@/components/emcn'
import { canEditUsageLimit } from '@/lib/billing/subscriptions/utils'
import { isHosted } from '@/lib/core/config/feature-flags'
import { useSubscriptionData, useUpdateUsageLimit } from '@/hooks/queries/subscription'
import { useCopilotStore } from '@/stores/panel/copilot/store'
const LIMIT_INCREMENTS = [0, 50, 100] as const
function roundUpToNearest50(value: number): number {
return Math.ceil(value / 50) * 50
}
export function UsageLimitActions() {
const { data: subscriptionData } = useSubscriptionData()
const updateUsageLimitMutation = useUpdateUsageLimit()
const subscription = subscriptionData?.data
const canEdit = subscription ? canEditUsageLimit(subscription) : false
const [selectedAmount, setSelectedAmount] = useState<number | null>(null)
const [isHidden, setIsHidden] = useState(false)
const currentLimit = subscription?.usage_limit ?? 0
const baseLimit = roundUpToNearest50(currentLimit) || 50
const limitOptions = LIMIT_INCREMENTS.map((increment) => baseLimit + increment)
const handleUpdateLimit = async (newLimit: number) => {
setSelectedAmount(newLimit)
try {
await updateUsageLimitMutation.mutateAsync({ limit: newLimit })
setIsHidden(true)
const { messages, sendMessage } = useCopilotStore.getState()
const lastUserMessage = [...messages].reverse().find((m) => m.role === 'user')
if (lastUserMessage) {
const filteredMessages = messages.filter(
(m) => !(m.role === 'assistant' && m.errorType === 'usage_limit')
)
useCopilotStore.setState({ messages: filteredMessages })
await sendMessage(lastUserMessage.content, {
fileAttachments: lastUserMessage.fileAttachments,
contexts: lastUserMessage.contexts,
messageId: lastUserMessage.id,
})
}
} catch {
setIsHidden(false)
} finally {
setSelectedAmount(null)
}
}
const handleNavigateToUpgrade = () => {
if (isHosted) {
window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'subscription' } }))
} else {
window.open('https://www.sim.ai', '_blank')
}
}
if (isHidden) {
return null
}
if (!isHosted || !canEdit) {
return (
<Button onClick={handleNavigateToUpgrade} variant='default'>
Upgrade
</Button>
)
}
return (
<>
{limitOptions.map((limit) => {
const isLoading = updateUsageLimitMutation.isPending && selectedAmount === limit
const isDisabled = updateUsageLimitMutation.isPending
return (
<Button
key={limit}
onClick={() => handleUpdateLimit(limit)}
disabled={isDisabled}
variant='default'
>
{isLoading ? <Loader2 className='mr-1 h-3 w-3 animate-spin' /> : null}${limit}
</Button>
)
})}
</>
)
}

View File

@@ -9,6 +9,7 @@ import {
SmoothStreamingText,
StreamingIndicator,
ThinkingBlock,
UsageLimitActions,
} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components'
import CopilotMarkdownRenderer from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/copilot-message/components/markdown-renderer'
import {
@@ -458,6 +459,12 @@ const CopilotMessage: FC<CopilotMessageProps> = memo(
<StreamingIndicator />
)}
{message.errorType === 'usage_limit' && (
<div className='mt-3 flex gap-1.5'>
<UsageLimitActions />
</div>
)}
{/* Action buttons for completed messages */}
{!isStreaming && cleanTextContent && (
<div className='flex items-center gap-[8px] pt-[8px]'>

View File

@@ -533,7 +533,11 @@ function createStreamingMessage(): CopilotMessage {
}
}
function createErrorMessage(messageId: string, content: string): CopilotMessage {
function createErrorMessage(
messageId: string,
content: string,
errorType?: 'usage_limit' | 'unauthorized' | 'forbidden' | 'rate_limit' | 'upgrade_required'
): CopilotMessage {
return {
id: messageId,
role: 'assistant',
@@ -546,6 +550,7 @@ function createErrorMessage(messageId: string, content: string): CopilotMessage
timestamp: Date.now(),
},
],
errorType,
}
}
@@ -2066,23 +2071,35 @@ export const useCopilotStore = create<CopilotStore>()(
// Check for specific status codes and provide custom messages
let errorContent = result.error || 'Failed to send message'
let errorType:
| 'usage_limit'
| 'unauthorized'
| 'forbidden'
| 'rate_limit'
| 'upgrade_required'
| undefined
if (result.status === 401) {
errorContent =
'_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._'
errorType = 'unauthorized'
} else if (result.status === 402) {
errorContent =
'_Usage limit exceeded. To continue using this service, upgrade your plan or top up on credits._'
'_Usage limit exceeded. To continue using this service, upgrade your plan or increase your usage limit to:_'
errorType = 'usage_limit'
} else if (result.status === 403) {
errorContent =
'_Provider config not allowed for non-enterprise users. Please remove the provider config and try again_'
errorType = 'forbidden'
} else if (result.status === 426) {
errorContent =
'_Please upgrade to the latest version of the Sim platform to continue using the copilot._'
errorType = 'upgrade_required'
} else if (result.status === 429) {
errorContent = '_Provider rate limit exceeded. Please try again later._'
errorType = 'rate_limit'
}
const errorMessage = createErrorMessage(streamingMessage.id, errorContent)
const errorMessage = createErrorMessage(streamingMessage.id, errorContent, errorType)
set((state) => ({
messages: state.messages.map((m) => (m.id === streamingMessage.id ? errorMessage : m)),
error: errorContent,

View File

@@ -39,6 +39,7 @@ export interface CopilotMessage {
>
fileAttachments?: MessageFileAttachment[]
contexts?: ChatContext[]
errorType?: 'usage_limit' | 'unauthorized' | 'forbidden' | 'rate_limit' | 'upgrade_required'
}
// Contexts attached to a user message