Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/sim/app/extension/[workspaceId]/workspace.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { isApiClientError } from '@/lib/api/client/errors'
import { ExtensionChat } from '@/app/extension/[workspaceId]/chat'
import ExtensionLoading from '@/app/extension/loading'
import { ExtensionSignIn } from '@/app/extension/sign-in'
import { ChatPlanModal } from '@/app/workspace/[workspaceId]/home/components/chat-plan-modal'
import { BlockVisibilityLoader } from '@/app/workspace/[workspaceId]/providers/block-visibility-loader'
import { CustomBlocksLoader } from '@/app/workspace/[workspaceId]/providers/custom-blocks-loader'
import { ProviderModelsLoader } from '@/app/workspace/[workspaceId]/providers/provider-models-loader'
Expand Down Expand Up @@ -65,6 +66,7 @@ export function ExtensionWorkspace({ workspaceId }: ExtensionWorkspaceProps) {
>
<ExtensionChat workspaceId={workspaceId} workspaceName={data.workspace.name} />
</PermissionAccessBoundary>
<ChatPlanModal />
</WorkspacePermissionsProvider>
</BrandingProvider>
</WorkspaceHostProvider>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { usePathname } from 'next/navigation'
import { getDesktopBridge } from '@/lib/desktop'
import { SidebarChromeProvider } from '@/app/workspace/[workspaceId]/components/workspace-chrome/sidebar-chrome-context'
import { useSidebarPeek } from '@/app/workspace/[workspaceId]/components/workspace-chrome/use-sidebar-peek'
import { ChatPlanModal } from '@/app/workspace/[workspaceId]/home/components/chat-plan-modal'
import { SidebarTooltip } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/sidebar-tooltip'
import { SIDEBAR_NO_MOTION_CLASS } from '@/app/workspace/[workspaceId]/w/components/sidebar/constants'
import { isMobileViewport, useIsMobile } from '@/hooks/use-is-mobile'
Expand Down Expand Up @@ -446,6 +447,7 @@ export function WorkspaceChrome({
</div>
)}
{!isFullscreen && <TitleBarHistoryNav />}
<ChatPlanModal />
</div>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
'use client'

import {
ChipModal,
ChipModalBody,
ChipModalDescription,
ChipModalFooter,
ChipModalHeader,
} from '@sim/emcn'
import { useRouter } from 'next/navigation'
import { CHAT_PLAN_REQUIRED_UPGRADE } from '@/lib/billing/chat-plan'
import { rememberSettingsReturnUrl } from '@/lib/navigation/settings-return'
import { usePlanUpgradeLink } from '@/app/workspace/[workspaceId]/home/hooks/use-plan-upgrade-link'
import { useChatPlanModalStore } from '@/stores/modals/chat-plan/store'

/**
* Answers a send the workspace's plan refuses: Chat is on paid plans, with the way to upgrade
* for a viewer who may, and who to ask for one who may not. Render it once per workspace shell.
*/
export function ChatPlanModal() {
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
const isOpen = useChatPlanModalStore((state) => state.isOpen)
const close = useChatPlanModalStore((state) => state.close)

return (
<ChipModal
open={isOpen}
onOpenChange={(open) => !open && close()}
srTitle='Paid Plan Required'
size='sm'
>
<ChatPlanModalContent onClose={close} />
</ChipModal>
)
}

interface ChatPlanModalContentProps {
onClose: () => void
}

/** Mounted only while the modal shows, so a closed modal reads no session or billing context. */
function ChatPlanModalContent({ onClose }: ChatPlanModalContentProps) {
const router = useRouter()
const { href, opensInNewTab, buttonLabel, canManageBilling, unavailableMessage } =
usePlanUpgradeLink(CHAT_PLAN_REQUIRED_UPGRADE)

const upgrade = () => {
onClose()
if (opensInNewTab) {
window.open(href, '_blank', 'noopener,noreferrer')
return
}
rememberSettingsReturnUrl(href)
router.push(href)
}

return (
<>
<ChipModalHeader onClose={onClose}>Paid Plan Required</ChipModalHeader>
<ChipModalBody>
<ChipModalDescription>
{CHAT_PLAN_REQUIRED_UPGRADE.message}
{!canManageBilling && ` ${unavailableMessage}`}
</ChipModalDescription>
</ChipModalBody>
{canManageBilling ? (
<ChipModalFooter
onCancel={onClose}
primaryAction={{ label: buttonLabel, onClick: upgrade }}
/>
) : (
<ChipModalFooter onCancel={onClose} cancelLabel='Close' defaultAction='dismiss' />
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { ChatPlanModal } from './chat-plan-modal'

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export { ChatMessageAttachments } from './chat-message-attachments'
export { ChatPlanNotice } from './chat-plan-notice'
export { ChatSurfaceProvider, useChatSurface } from './chat-surface-context'
export { ContextMentionIcon } from './context-mention-icon'
export { CreditsChip } from './credits-chip'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,6 @@ export {
credentialTagHasVisibleCard,
formatCredentialSubmissionMessage,
isHttpUrl,
PlanUsageLimitDisplay,
parseCredentialSubmissionMessage,
parseCredentialSubmissionProgress,
parseCredentialTagBody,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,9 @@ import {
} from '@sim/emcn/icons'
import { isRecordLike, omit } from '@sim/utils/object'
import { useParams } from 'next/navigation'
import { getOrganizationSettingsHref } from '@/components/settings/navigation'
import { useSettingsNavigationTarget } from '@/components/settings/settings-navigation-provider'
import { useSession } from '@/lib/auth/client'
import { CHAT_PLAN_REQUIRED_REASON } from '@/lib/billing/chat-plan'
import {
buildHostedUpgradeUrl,
buildUpgradeHref,
HOSTED_BILLING_SETTINGS_URL,
} from '@/lib/billing/upgrade-reasons'
import { canManageWorkspaceBilling } from '@/lib/billing/workspace-permissions'
import { isBrowserAgentAvailable, sendBrowserPanelAction } from '@/lib/browser-agent/transport'
import { useDeploymentShape } from '@/lib/core/config/deployment-shape'
import { isSafeHttpUrl } from '@/lib/core/utils/urls'
import { readLatestOAuthChatAttempt } from '@/lib/credentials/oauth-chat-attempt'
import { getDesktopBridge } from '@/lib/desktop'
Expand All @@ -64,7 +55,6 @@ import { OAUTH_PROVIDERS } from '@/lib/oauth/oauth'
import { getServiceConfigByProviderId } from '@/lib/oauth/utils'
import { organizationSecretNameSchema } from '@/lib/organization-secrets/validation'
import { finishTerminalHandoff, isTerminalAvailable } from '@/lib/terminal/transport'
import { useOptionalOrganizationContext } from '@/app/o/[organizationId]/providers/organization-provider'
import { useChatSurface } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import { ContextMentionIcon } from '@/app/workspace/[workspaceId]/home/components/context-mention-icon'
import {
Expand Down Expand Up @@ -93,6 +83,7 @@ import {
} from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/use-oauth-chip-connection'
import { usePersonalCredentialConnection } from '@/app/workspace/[workspaceId]/home/components/message-content/components/special-tags/use-personal-credential-connection'
import { ResourceWorkspaceHost } from '@/app/workspace/[workspaceId]/home/components/resource-workspace-host'
import { usePlanUpgradeLink } from '@/app/workspace/[workspaceId]/home/hooks/use-plan-upgrade-link'
import type {
ChatMessageContext,
MothershipResource,
Expand Down Expand Up @@ -121,7 +112,6 @@ import { useTablesList } from '@/hooks/queries/tables'
import { useWorkflows } from '@/hooks/queries/workflows'
import { useWorkspaceFileAtPath, useWorkspaceFileLookup } from '@/hooks/queries/workspace-files'
import { useWorkspaceUsageGate } from '@/hooks/queries/workspace-usage'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'

export interface OptionsItemData {
title: string
Expand Down Expand Up @@ -3500,45 +3490,14 @@ interface PlanUsageLimitDisplayProps {
* The payer's plan limit, or a free plan that does not include Chat: the card links to where
* the plan or the limit can change, and a workspace's plan to its upgrade page.
*/
export function PlanUsageLimitDisplay({ data }: PlanUsageLimitDisplayProps) {
const { data: session } = useSession()
function PlanUsageLimitDisplay({ data }: PlanUsageLimitDisplayProps) {
const hostContext = useOptionalWorkspaceHostContext()
const organizationContext = useOptionalOrganizationContext()
const { getSettingsHref } = useSettingsNavigation()
const { hosted } = useDeploymentShape()
const navigationTarget = useSettingsNavigationTarget()
const opensInNewTab = !hosted || navigationTarget === '_blank'
const { href, opensInNewTab, buttonLabel, canManageBilling, unavailableMessage } =
usePlanUpgradeLink(data)
const planRequired = data.reason === CHAT_PLAN_REQUIRED_REASON
const buttonLabel = data.action === 'upgrade_plan' ? 'Upgrade Plan' : 'Increase Limit'

// Self-hosted plan and limit both live on the hosted account, so local
// workspace billing roles say nothing about who may change them.
const href = hosted
? organizationContext
? getOrganizationSettingsHref(organizationContext.organization.id, 'billing')
: planRequired && hostContext
? buildUpgradeHref(hostContext.workspace.id)
: getSettingsHref({ section: 'billing' })
: data.action === 'upgrade_plan'
? buildHostedUpgradeUrl()
: HOSTED_BILLING_SETTINGS_URL
const canManageBilling =
!hosted ||
(organizationContext
? organizationContext.viewer.isAdmin
: Boolean(hostContext && canManageWorkspaceBilling(hostContext, session?.user?.id)))
const usageGate = useWorkspaceUsageGate(
data.action === 'increase_limit' && !canManageBilling ? hostContext?.workspace.id : undefined
)
const action = planRequired
? 'upgrade this workspace’s plan'
: 'manage this workspace’s usage limits'
const unavailableMessage =
organizationContext && !planRequired
? 'Contact an organization admin to manage usage limits.'
: organizationContext || hostContext?.hostOrganizationId
? `Contact an organization admin to ${action}.`
: `Only the workspace owner can ${action}.`

return (
<InteractionCard
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,6 @@ import { inter } from '@/app/_styles/fonts/inter/inter'
import { FindBar } from '@/app/workspace/[workspaceId]/components/find-bar'
import { MessageActions } from '@/app/workspace/[workspaceId]/components/message-actions'
import { ChatMessageAttachments } from '@/app/workspace/[workspaceId]/home/components/chat-message-attachments'
import { ChatPlanNotice } from '@/app/workspace/[workspaceId]/home/components/chat-plan-notice'
import { ChatSurfaceProvider } from '@/app/workspace/[workspaceId]/home/components/chat-surface-context'
import { ComposerScrollArea } from '@/app/workspace/[workspaceId]/home/components/composer-scroll-area'
import {
Expand Down Expand Up @@ -1037,7 +1036,6 @@ export function MothershipChat({
>
<div className={styles.footerInner}>
<ComputerUseActivity />
{!isLoading && <ChatPlanNotice />}
<QueuedMessages
messageQueue={messageQueue}
editingQueuedId={editingQueuedId}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ import { useHydrated } from '@/hooks/use-hydrated'
import { isMobileViewport } from '@/hooks/use-is-mobile'
import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
import { useVoiceInput } from '@/hooks/use-voice-input'
import { useChatPlanModalStore } from '@/stores/modals/chat-plan/store'
import { type DraftPayload, useMothershipDraftsStore } from '@/stores/mothership-drafts/store'
import type { ChatContext } from '@/stores/panel'

Expand Down Expand Up @@ -322,8 +323,7 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
const canSubmit =
(editor.value.trim().length > 0 || hasFiles || hasAdditionalAttachments) &&
!isSending &&
!hasUploadingFiles &&
!chatPlanRequired
!hasUploadingFiles

/**
* Sync the editor when the `defaultValue` prop changes post-mount — e.g.
Expand Down Expand Up @@ -532,7 +532,10 @@ const UserInputImpl = forwardRef<UserInputHandle, UserInputProps>(function UserI
}, [resetTranscript, history.reset])

const handleSubmit = useCallback(() => {
if (chatPlanRequired) return
if (chatPlanRequired) {
useChatPlanModalStore.getState().open()
return
}
const currentFiles = filesRef.current
const currentEditor = editorRef.current

Expand Down
2 changes: 0 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/home/home.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,6 @@ import { useMarkMothershipChatRead } from '@/hooks/queries/mothership-chats'
import { useOAuthReturnRouter } from '@/hooks/use-oauth-return'
import type { ChatContext } from '@/stores/panel'
import {
ChatPlanNotice,
ChatSurfaceProvider,
CreditsChip,
MothershipChat,
Expand Down Expand Up @@ -443,7 +442,6 @@ function HomeContent({ chatId, userName, userId }: HomeProps) {
What should we get done{firstName ? `, ${firstName}` : ''}?
</h1>
<div ref={initialViewInputRef} className='relative w-full max-w-chat'>
<ChatPlanNotice />
<ChatSurfaceProvider
userId={userId}
onContextAdd={handleContextAdd}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
import { useOptionalWorkspaceHostContext } from '@/app/workspace/[workspaceId]/providers/workspace-host-provider'

/**
* Whether this workspace's payer may not use Chat, so the composer shows the upgrade card and
* holds sends that run admission would refuse.
* Whether this workspace's payer may not use Chat, so sends that run admission would refuse are
* held and answered with the Chat plan modal instead.
*
* The verdict is the host context's `features.chat`, which the server resolves with the same
* check admission runs. A surface without a workspace host (an organization conversation), or a
Expand Down
11 changes: 9 additions & 2 deletions apps/sim/app/workspace/[workspaceId]/home/hooks/use-chat.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@ import { getWorkflowListQueryOptions } from '@/hooks/queries/utils/workflow-list
import { workflowKeys } from '@/hooks/queries/workflows'
import { snapAllSmoothText } from '@/hooks/use-smooth-text'
import { useChatPanelStore } from '@/stores/chat-panel/store'
import { useChatPlanModalStore } from '@/stores/modals/chat-plan/store'
import {
useMothershipEffortStore,
useMothershipPlanEffortStore,
Expand Down Expand Up @@ -4441,7 +4442,10 @@ export function useChat(
options?: SendMessageOptions
) => {
if ((!message.trim() && !fileAttachments?.length) || !scopeKey) return
if (chatPlanRequired) return
if (chatPlanRequired) {
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
useChatPlanModalStore.getState().open()
Comment thread
waleedlatif1 marked this conversation as resolved.
Resolved
return
}

const queueStore = useMothershipQueueStore.getState()
const activeChatKey = chatKeyRef.current
Expand Down Expand Up @@ -5302,7 +5306,10 @@ export function useChat(
const msg = id === undefined ? queue?.[0] : queue?.find((queued) => queued.id === id)
if (!msg || queueState.editing[chatKey] === msg.id) return
if (queuedMessageDispatchIds.has(msg.id)) return
if (chatPlanRequired) return
if (chatPlanRequired) {
useChatPlanModalStore.getState().open()
return
}
/* Sent by hand, it still must not run a second turn: if the history shows it
accepted, it is already in the chat. A history that cannot be read does not
hold it back here; the user asked to send it, and the server deduplicates it
Expand Down
Loading
Loading