From 86928c9d4adb0e6127775c6dc989f3aa9e0c8c3f Mon Sep 17 00:00:00 2001 From: Vukasin Date: Fri, 18 Jul 2025 00:16:46 +0200 Subject: [PATCH 1/3] feat: usage snack --- .../[projectId]/thread/[threadId]/page.tsx | 3 +- .../thread/chat-input/chat-input.tsx | 383 ++++++++++-------- .../thread/chat-input/chat-snack.tsx | 153 +++++++ .../chat-input/floating-tool-preview.tsx | 40 +- .../thread/chat-input/usage-preview.tsx | 129 ++++++ 5 files changed, 527 insertions(+), 181 deletions(-) create mode 100644 frontend/src/components/thread/chat-input/chat-snack.tsx create mode 100644 frontend/src/components/thread/chat-input/usage-preview.tsx diff --git a/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx b/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx index 6429c760..daf92adb 100644 --- a/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx +++ b/frontend/src/app/(dashboard)/projects/[projectId]/thread/[threadId]/page.tsx @@ -619,7 +619,7 @@ export default function ThreadPage({ )} */} - + diff --git a/frontend/src/components/thread/chat-input/chat-input.tsx b/frontend/src/components/thread/chat-input/chat-input.tsx index 0d4ad94f..cfc69003 100644 --- a/frontend/src/components/thread/chat-input/chat-input.tsx +++ b/frontend/src/components/thread/chat-input/chat-input.tsx @@ -7,6 +7,8 @@ import React, { forwardRef, useImperativeHandle, } from 'react'; + +import { motion } from 'framer-motion'; import { Card, CardContent } from '@/components/ui/card'; import { handleFiles } from './file-upload-handler'; import { MessageInput } from './message-input'; @@ -14,13 +16,18 @@ import { AttachmentGroup } from '../attachment-group'; import { useModelSelection } from './_use-model-selection'; import { useFileDelete } from '@/hooks/react-query/files'; import { useQueryClient } from '@tanstack/react-query'; -import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview'; -import { Settings2, Sparkles, Brain, ChevronRight, Zap, Workflow, Database, Wrench } from 'lucide-react'; +import { ToolCallInput } from './floating-tool-preview'; +import { ChatSnack } from './chat-snack'; +import { Brain, ChevronRight, Zap, Workflow, Database, Wrench, X } from 'lucide-react'; import { FaGoogle, FaDiscord } from 'react-icons/fa'; import { SiNotion } from 'react-icons/si'; import { AgentConfigModal } from '@/components/agents/agent-config-modal'; import { PipedreamRegistry } from '@/components/agents/pipedream/pipedream-registry'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; +import { useSubscription } from '@/hooks/react-query/subscriptions/use-subscriptions'; +import { isLocalMode } from '@/lib/config'; +import { Button } from '@/components/ui/button'; +import { AnimatePresence } from 'framer-motion'; export interface ChatInputHandles { getPendingFiles: () => File[]; @@ -56,6 +63,7 @@ export interface ChatInputProps { enableAdvancedConfig?: boolean; onConfigureAgent?: (agentId: string) => void; hideAgentSelection?: boolean; + defaultShowSnackbar?: 'tokens' | 'upgrade' | false; } export interface UploadedFile { @@ -66,6 +74,8 @@ export interface UploadedFile { localUrl?: string; } + + export const ChatInput = forwardRef( ( { @@ -94,6 +104,7 @@ export const ChatInput = forwardRef( enableAdvancedConfig = false, onConfigureAgent, hideAgentSelection = false, + defaultShowSnackbar = false, }, ref, ) => { @@ -110,6 +121,7 @@ export const ChatInput = forwardRef( const [configModalOpen, setConfigModalOpen] = useState(false); const [configModalTab, setConfigModalTab] = useState('integrations'); const [registryDialogOpen, setRegistryDialogOpen] = useState(false); + const [showSnackbar, setShowSnackbar] = useState(defaultShowSnackbar); const { selectedModel, @@ -121,9 +133,20 @@ export const ChatInput = forwardRef( refreshCustomModels, } = useModelSelection(); + const { data: subscriptionData } = useSubscription(); const deleteFileMutation = useFileDelete(); const queryClient = useQueryClient(); + // Simple logic: show usage preview if we have subscription data and not in local mode + const shouldShowUsage = !isLocalMode() && subscriptionData; + + // Auto-show usage preview when we have subscription data + useEffect(() => { + if (shouldShowUsage && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) { + setShowSnackbar('upgrade'); + } + }, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage]); + const textareaRef = useRef(null); const fileInputRef = useRef(null); const hasLoadedFromLocalStorage = useRef(false); @@ -137,7 +160,7 @@ export const ChatInput = forwardRef( if (typeof window !== 'undefined' && onAgentSelect && !hasLoadedFromLocalStorage.current) { const urlParams = new URLSearchParams(window.location.search); const hasAgentIdInUrl = urlParams.has('agent_id'); - + if (!selectedAgentId && !hasAgentIdInUrl) { const savedAgentId = localStorage.getItem('lastSelectedAgentId'); if (savedAgentId) { @@ -283,185 +306,195 @@ export const ChatInput = forwardRef( setIsDraggingOver(false); }; + + return ( -
- { })} - agentName={agentName} - isVisible={showToolPreview} - /> - { - e.preventDefault(); - e.stopPropagation(); - setIsDraggingOver(false); - if (fileInputRef.current && e.dataTransfer.files.length > 0) { - const files = Array.from(e.dataTransfer.files); - handleFiles( - files, - sandboxId, - setPendingFiles, - setUploadedFiles, - setIsUploading, - messages, - queryClient, - ); - } - }} - > -
- - - +
+ setShowSnackbar(false)} + isVisible={showToolPreview || !!showSnackbar} + /> + { + e.preventDefault(); + e.stopPropagation(); + setIsDraggingOver(false); + if (fileInputRef.current && e.dataTransfer.files.length > 0) { + const files = Array.from(e.dataTransfer.files); + handleFiles( + files, + sandboxId, + setPendingFiles, + setUploadedFiles, + setIsUploading, + messages, + queryClient, + ); + } + }} + > - fileInputRef={fileInputRef} - isUploading={isUploading} - sandboxId={sandboxId} - setPendingFiles={setPendingFiles} - setUploadedFiles={setUploadedFiles} - setIsUploading={setIsUploading} - hideAttachments={hideAttachments} - messages={messages} - selectedModel={selectedModel} - onModelChange={handleModelChange} - modelOptions={modelOptions} - subscriptionStatus={subscriptionStatus} - canAccessModel={canAccessModel} - refreshCustomModels={refreshCustomModels} - isLoggedIn={isLoggedIn} +
+ + + - - - {enableAdvancedConfig && selectedAgentId && ( -
-
-
-
- Integrations - - -
- - - -
- - - -
- - - -
- - + Integrations + + +
+ + + +
+ + + +
+ + + +
+ + +
-
- )} -
- - - - - - Integrations - - { - console.log('Tools selected:', { profileId, selectedTools, appName, appSlug }); - }} - /> - - + )} +
+ + + + + + Integrations + + { + console.log('Tools selected:', { profileId, selectedTools, appName, appSlug }); + }} + /> + + +
); }, diff --git a/frontend/src/components/thread/chat-input/chat-snack.tsx b/frontend/src/components/thread/chat-input/chat-snack.tsx new file mode 100644 index 00000000..c52616b5 --- /dev/null +++ b/frontend/src/components/thread/chat-input/chat-snack.tsx @@ -0,0 +1,153 @@ +import React from 'react'; +import { motion, AnimatePresence } from 'framer-motion'; +import { X } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { UsagePreview } from './usage-preview'; +import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview'; + +export interface ChatSnackProps { + // Tool preview props + toolCalls?: ToolCallInput[]; + toolCallIndex?: number; + onExpandToolPreview?: () => void; + agentName?: string; + showToolPreview?: boolean; + + // Usage preview props + showUsagePreview?: 'tokens' | 'upgrade' | false; + subscriptionData?: any; + onCloseUsage?: () => void; + + // General props + isVisible?: boolean; +} + +const SNACK_LAYOUT_ID = 'chat-snack-float'; +const SNACK_CONTENT_LAYOUT_ID = 'chat-snack-content'; + +export const ChatSnack: React.FC = ({ + toolCalls = [], + toolCallIndex = 0, + onExpandToolPreview, + agentName, + showToolPreview = false, + showUsagePreview = false, + subscriptionData, + onCloseUsage, + isVisible = false, +}) => { + const [currentView, setCurrentView] = React.useState(0); + + // Determine what notifications we have + const notifications = []; + if (showToolPreview && toolCalls.length > 0) { + notifications.push('tool'); + } + if (showUsagePreview) { + notifications.push('usage'); + } + + const totalNotifications = notifications.length; + const hasMultiple = totalNotifications > 1; + + // Reset currentView when notifications change + React.useEffect(() => { + if (currentView >= totalNotifications && totalNotifications > 0) { + setCurrentView(0); + } + }, [totalNotifications, currentView]); + + // Auto-cycle through notifications + React.useEffect(() => { + if (!hasMultiple || !isVisible) return; + + const interval = setInterval(() => { + setCurrentView((prev) => (prev + 1) % totalNotifications); + }, 20000); + + return () => clearInterval(interval); + }, [hasMultiple, isVisible, totalNotifications, currentView]); // Reset timer when currentView changes + + if (!isVisible || totalNotifications === 0) return null; + + const currentNotification = notifications[currentView]; + + const renderContent = () => { + if (currentNotification === 'tool' && showToolPreview) { + return ( + { })} + agentName={agentName} + isVisible={true} + showIndicators={hasMultiple} + indicatorIndex={currentView} + indicatorTotal={totalNotifications} + onIndicatorClick={(index) => setCurrentView(index)} + /> + ); + } + + if (currentNotification === 'usage' && showUsagePreview) { + return ( + + + { + if (onCloseUsage) onCloseUsage(); + // If there are other notifications, switch to them + if (totalNotifications > 1) { + const remainingNotifications = notifications.filter(n => n !== 'usage'); + if (remainingNotifications.length > 0) { + setCurrentView(0); // Switch to first remaining notification + } + } + }} + hasMultiple={hasMultiple} + showIndicators={hasMultiple} + currentIndex={currentView} + totalCount={totalNotifications} + onIndicatorClick={(index) => setCurrentView(index)} + /> + + + ); + } + + return null; + }; + + return ( + + {isVisible && ( + + {renderContent()} + + )} + + ); +}; diff --git a/frontend/src/components/thread/chat-input/floating-tool-preview.tsx b/frontend/src/components/thread/chat-input/floating-tool-preview.tsx index 17bcd54b..3272b427 100644 --- a/frontend/src/components/thread/chat-input/floating-tool-preview.tsx +++ b/frontend/src/components/thread/chat-input/floating-tool-preview.tsx @@ -25,6 +25,11 @@ interface FloatingToolPreviewProps { onExpand: () => void; agentName?: string; isVisible: boolean; + // Indicators for multiple notification types (not tool calls) + showIndicators?: boolean; + indicatorIndex?: number; + indicatorTotal?: number; + onIndicatorClick?: (index: number) => void; } const FLOATING_LAYOUT_ID = 'tool-panel-float'; @@ -61,6 +66,10 @@ export const FloatingToolPreview: React.FC = ({ onExpand, agentName, isVisible, + showIndicators = false, + indicatorIndex = 0, + indicatorTotal = 1, + onIndicatorClick, }) => { const [isExpanding, setIsExpanding] = React.useState(false); const currentToolCall = toolCalls[currentIndex]; @@ -137,11 +146,6 @@ export const FloatingToolPreview: React.FC = ({

{getUserFriendlyToolName(toolName)}

-
- - {currentIndex + 1}/{totalCalls} - -
@@ -164,6 +168,32 @@ export const FloatingToolPreview: React.FC = ({
+ {/* Apple-style notification indicators - only for multiple notification types */} + {showIndicators && indicatorTotal === 2 && ( + + )} + diff --git a/frontend/src/components/thread/chat-input/usage-preview.tsx b/frontend/src/components/thread/chat-input/usage-preview.tsx new file mode 100644 index 00000000..1a959a1f --- /dev/null +++ b/frontend/src/components/thread/chat-input/usage-preview.tsx @@ -0,0 +1,129 @@ +import React from 'react'; +import { motion } from 'framer-motion'; +import { X, Zap } from 'lucide-react'; +import { cn } from '@/lib/utils'; +import { isLocalMode } from '@/lib/config'; +import { Button } from '@/components/ui/button'; + +export interface UsagePreviewProps { + type: 'tokens' | 'upgrade'; + subscriptionData?: any; + onClose?: () => void; + hasMultiple?: boolean; + showIndicators?: boolean; + currentIndex?: number; + totalCount?: number; + onIndicatorClick?: (index: number) => void; +} + +export const UsagePreview: React.FC = ({ + type, + subscriptionData, + onClose, + hasMultiple = false, + showIndicators = false, + currentIndex = 0, + totalCount = 1, + onIndicatorClick, +}) => { + if (isLocalMode()) return null; + + const formatCurrency = (amount: number) => { + return `$${amount.toFixed(2)}`; + }; + + const getUsageDisplay = () => { + if (!subscriptionData) return 'Loading usage...'; + + const current = subscriptionData.current_usage || 0; + const limit = subscriptionData.cost_limit || 0; + + if (limit === 0) return 'No usage limit set'; + + const isOverLimit = current > limit; + const usageText = `${formatCurrency(current)} / ${formatCurrency(limit)}`; + + if (isOverLimit) { + return `${usageText} (over limit)`; + } + + return usageText; + }; + + const isOverLimit = () => { + if (!subscriptionData) return false; + const current = subscriptionData.current_usage || 0; + const limit = subscriptionData.cost_limit || 0; + return current > limit; + }; + + return ( +
+ {/* Icon */} +
+ + + +
+ + {/* Content */} +
+ +

+ Upgrade for more usage +

+
+ + +
+ + {getUsageDisplay()} + + +
+ + {/* Apple-style notification indicators - only for multiple notification types */} + {showIndicators && totalCount === 2 && ( + + )} + + +
+ ); +}; \ No newline at end of file From 9c62b8e805270c3064d5a00aeeb7884a7398079d Mon Sep 17 00:00:00 2001 From: Vukasin Date: Fri, 18 Jul 2025 00:21:24 +0200 Subject: [PATCH 2/3] fix: showing snack --- frontend/src/components/thread/chat-input/chat-input.tsx | 2 +- frontend/src/components/thread/chat-input/message-input.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/frontend/src/components/thread/chat-input/chat-input.tsx b/frontend/src/components/thread/chat-input/chat-input.tsx index cfc69003..20a55c8e 100644 --- a/frontend/src/components/thread/chat-input/chat-input.tsx +++ b/frontend/src/components/thread/chat-input/chat-input.tsx @@ -142,7 +142,7 @@ export const ChatInput = forwardRef( // Auto-show usage preview when we have subscription data useEffect(() => { - if (shouldShowUsage && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) { + if (shouldShowUsage && defaultShowSnackbar !== false && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) { setShowSnackbar('upgrade'); } }, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage]); diff --git a/frontend/src/components/thread/chat-input/message-input.tsx b/frontend/src/components/thread/chat-input/message-input.tsx index ded27ec4..2ffff07d 100644 --- a/frontend/src/components/thread/chat-input/message-input.tsx +++ b/frontend/src/components/thread/chat-input/message-input.tsx @@ -156,7 +156,7 @@ export const MessageInput = forwardRef( const renderDropdown = () => { if (isLoggedIn) { const showAdvancedFeatures = enableAdvancedConfig || (customAgentsEnabled && !flagsLoading); - + return (
{showAdvancedFeatures && !hideAgentSelection && ( @@ -223,7 +223,7 @@ export const MessageInput = forwardRef(
- {subscriptionStatus === 'no_subscription' && !isLocalMode() && + {/* {subscriptionStatus === 'no_subscription' && !isLocalMode() && @@ -234,7 +234,7 @@ export const MessageInput = forwardRef( - } + } */}
{renderDropdown()} From 6468f0ab08c12cbac59b36e3f9b6b0c2ba401273 Mon Sep 17 00:00:00 2001 From: Vukasin Date: Fri, 18 Jul 2025 21:49:10 +0200 Subject: [PATCH 3/3] feat: add streaming and conditionaly show usage --- .../thread/chat-input/chat-input.tsx | 34 +++++++++++++++--- .../thread/chat-input/chat-snack.tsx | 31 +++++++++++++--- .../thread/chat-input/message-input.tsx | 4 +-- .../thread/chat-input/usage-preview.tsx | 5 ++- .../subscriptions/use-subscriptions.ts | 35 +++++++++++++++++++ 5 files changed, 97 insertions(+), 12 deletions(-) diff --git a/frontend/src/components/thread/chat-input/chat-input.tsx b/frontend/src/components/thread/chat-input/chat-input.tsx index 20a55c8e..575b1d47 100644 --- a/frontend/src/components/thread/chat-input/chat-input.tsx +++ b/frontend/src/components/thread/chat-input/chat-input.tsx @@ -24,10 +24,11 @@ import { SiNotion } from 'react-icons/si'; import { AgentConfigModal } from '@/components/agents/agent-config-modal'; import { PipedreamRegistry } from '@/components/agents/pipedream/pipedream-registry'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; -import { useSubscription } from '@/hooks/react-query/subscriptions/use-subscriptions'; +import { useSubscriptionWithStreaming } from '@/hooks/react-query/subscriptions/use-subscriptions'; import { isLocalMode } from '@/lib/config'; import { Button } from '@/components/ui/button'; import { AnimatePresence } from 'framer-motion'; +import { BillingModal } from '@/components/billing/billing-modal'; export interface ChatInputHandles { getPendingFiles: () => File[]; @@ -64,6 +65,7 @@ export interface ChatInputProps { onConfigureAgent?: (agentId: string) => void; hideAgentSelection?: boolean; defaultShowSnackbar?: 'tokens' | 'upgrade' | false; + showToLowCreditUsers?: boolean; } export interface UploadedFile { @@ -105,6 +107,7 @@ export const ChatInput = forwardRef( onConfigureAgent, hideAgentSelection = false, defaultShowSnackbar = false, + showToLowCreditUsers = true, }, ref, ) => { @@ -122,6 +125,7 @@ export const ChatInput = forwardRef( const [configModalTab, setConfigModalTab] = useState('integrations'); const [registryDialogOpen, setRegistryDialogOpen] = useState(false); const [showSnackbar, setShowSnackbar] = useState(defaultShowSnackbar); + const [billingModalOpen, setBillingModalOpen] = useState(false); const { selectedModel, @@ -133,19 +137,34 @@ export const ChatInput = forwardRef( refreshCustomModels, } = useModelSelection(); - const { data: subscriptionData } = useSubscription(); + const { data: subscriptionData } = useSubscriptionWithStreaming(isAgentRunning); const deleteFileMutation = useFileDelete(); const queryClient = useQueryClient(); - // Simple logic: show usage preview if we have subscription data and not in local mode - const shouldShowUsage = !isLocalMode() && subscriptionData; + // Show usage preview logic: + // - Always show to free users when showToLowCreditUsers is true + // - For paid users, only show when they're at 70% or more of their cost limit (30% or below remaining) + const shouldShowUsage = !isLocalMode() && subscriptionData && showToLowCreditUsers && (() => { + // Free users: always show + if (subscriptionStatus === 'no_subscription') { + return true; + } + + // Paid users: only show when at 70% or more of cost limit + const currentUsage = subscriptionData.current_usage || 0; + const costLimit = subscriptionData.cost_limit || 0; + + if (costLimit === 0) return false; // No limit set + + return currentUsage >= (costLimit * 0.7); // 70% or more used (30% or less remaining) + })(); // Auto-show usage preview when we have subscription data useEffect(() => { if (shouldShowUsage && defaultShowSnackbar !== false && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) { setShowSnackbar('upgrade'); } - }, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage]); + }, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage, subscriptionStatus, showToLowCreditUsers]); const textareaRef = useRef(null); const fileInputRef = useRef(null); @@ -320,6 +339,7 @@ export const ChatInput = forwardRef( showUsagePreview={showSnackbar} subscriptionData={subscriptionData} onCloseUsage={() => setShowSnackbar(false)} + onOpenUpgrade={() => setBillingModalOpen(true)} isVisible={showToolPreview || !!showSnackbar} /> ( /> +
); diff --git a/frontend/src/components/thread/chat-input/chat-snack.tsx b/frontend/src/components/thread/chat-input/chat-snack.tsx index c52616b5..e73ce1b3 100644 --- a/frontend/src/components/thread/chat-input/chat-snack.tsx +++ b/frontend/src/components/thread/chat-input/chat-snack.tsx @@ -4,6 +4,7 @@ import { X } from 'lucide-react'; import { cn } from '@/lib/utils'; import { UsagePreview } from './usage-preview'; import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview'; +import { isLocalMode } from '@/lib/config'; export interface ChatSnackProps { // Tool preview props @@ -17,6 +18,7 @@ export interface ChatSnackProps { showUsagePreview?: 'tokens' | 'upgrade' | false; subscriptionData?: any; onCloseUsage?: () => void; + onOpenUpgrade?: () => void; // General props isVisible?: boolean; @@ -34,16 +36,21 @@ export const ChatSnack: React.FC = ({ showUsagePreview = false, subscriptionData, onCloseUsage, + onOpenUpgrade, isVisible = false, }) => { const [currentView, setCurrentView] = React.useState(0); - // Determine what notifications we have + // Determine what notifications we have - match exact rendering conditions const notifications = []; + + // Tool notification: only if we have tool calls and showToolPreview is true if (showToolPreview && toolCalls.length > 0) { notifications.push('tool'); } - if (showUsagePreview) { + + // Usage notification: must match ALL rendering conditions + if (showUsagePreview && !isLocalMode() && subscriptionData) { notifications.push('usage'); } @@ -89,7 +96,7 @@ export const ChatSnack: React.FC = ({ ); } - if (currentNotification === 'usage' && showUsagePreview) { + if (currentNotification === 'usage' && showUsagePreview && !isLocalMode()) { return ( = ({ > { + // Don't trigger if clicking on indicators or close button + const target = e.target as HTMLElement; + const isIndicatorClick = target.closest('[data-indicator-click]'); + const isCloseClick = target.closest('[data-close-click]'); + + if (!isIndicatorClick && !isCloseClick && onOpenUpgrade) { + onOpenUpgrade(); + } + }} > = ({ currentIndex={currentView} totalCount={totalNotifications} onIndicatorClick={(index) => setCurrentView(index)} + onOpenUpgrade={onOpenUpgrade} /> diff --git a/frontend/src/components/thread/chat-input/message-input.tsx b/frontend/src/components/thread/chat-input/message-input.tsx index 2ffff07d..8101e6d7 100644 --- a/frontend/src/components/thread/chat-input/message-input.tsx +++ b/frontend/src/components/thread/chat-input/message-input.tsx @@ -277,13 +277,13 @@ export const MessageInput = forwardRef(
- {subscriptionStatus === 'no_subscription' && !isLocalMode() && + {/* {subscriptionStatus === 'no_subscription' && !isLocalMode() &&

Upgrade for better performance

- } + } */}
); }, diff --git a/frontend/src/components/thread/chat-input/usage-preview.tsx b/frontend/src/components/thread/chat-input/usage-preview.tsx index 1a959a1f..ddf5e300 100644 --- a/frontend/src/components/thread/chat-input/usage-preview.tsx +++ b/frontend/src/components/thread/chat-input/usage-preview.tsx @@ -9,6 +9,7 @@ export interface UsagePreviewProps { type: 'tokens' | 'upgrade'; subscriptionData?: any; onClose?: () => void; + onOpenUpgrade?: () => void; hasMultiple?: boolean; showIndicators?: boolean; currentIndex?: number; @@ -20,6 +21,7 @@ export const UsagePreview: React.FC = ({ type, subscriptionData, onClose, + onOpenUpgrade, hasMultiple = false, showIndicators = false, currentIndex = 0, @@ -100,6 +102,7 @@ export const UsagePreview: React.FC = ({ {/* Apple-style notification indicators - only for multiple notification types */} {showIndicators && totalCount === 2 && ( )} -
diff --git a/frontend/src/hooks/react-query/subscriptions/use-subscriptions.ts b/frontend/src/hooks/react-query/subscriptions/use-subscriptions.ts index b0b394f4..0711e3fb 100644 --- a/frontend/src/hooks/react-query/subscriptions/use-subscriptions.ts +++ b/frontend/src/hooks/react-query/subscriptions/use-subscriptions.ts @@ -7,6 +7,8 @@ import { SubscriptionStatus, } from '@/lib/api'; import { subscriptionKeys } from './keys'; +import { useQuery } from '@tanstack/react-query'; +import { useState, useEffect } from 'react'; export const useSubscription = createQueryHook( subscriptionKeys.details(), @@ -17,6 +19,39 @@ export const useSubscription = createQueryHook( }, ); +// Smart subscription hook that adapts refresh based on streaming state +export const useSubscriptionWithStreaming = (isStreaming: boolean = false) => { + const [isVisible, setIsVisible] = useState(true); + + // Track page visibility + useEffect(() => { + const handleVisibilityChange = () => { + setIsVisible(!document.hidden); + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => document.removeEventListener('visibilitychange', handleVisibilityChange); + }, []); + + return useQuery({ + queryKey: subscriptionKeys.details(), + queryFn: getSubscription, + staleTime: 1000 * 60 * 2, // 2 minutes + refetchOnWindowFocus: true, + refetchInterval: (data) => { + // No refresh if tab is hidden + if (!isVisible) return false; + + // If actively streaming: refresh every 5s (costs are changing) + if (isStreaming) return 5 * 1000; + + // If visible but not streaming: refresh every 5min + return 5 * 60 * 1000; + }, + refetchIntervalInBackground: false, // Stop when tab backgrounded + }); +}; + export const useCreatePortalSession = createMutationHook( (params: { return_url: string }) => createPortalSession(params), {