mirror of https://github.com/kortix-ai/suna.git
feat: add streaming and conditionaly show usage
This commit is contained in:
parent
9c62b8e805
commit
6468f0ab08
|
@ -24,10 +24,11 @@ import { SiNotion } from 'react-icons/si';
|
||||||
import { AgentConfigModal } from '@/components/agents/agent-config-modal';
|
import { AgentConfigModal } from '@/components/agents/agent-config-modal';
|
||||||
import { PipedreamRegistry } from '@/components/agents/pipedream/pipedream-registry';
|
import { PipedreamRegistry } from '@/components/agents/pipedream/pipedream-registry';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
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 { isLocalMode } from '@/lib/config';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { AnimatePresence } from 'framer-motion';
|
import { AnimatePresence } from 'framer-motion';
|
||||||
|
import { BillingModal } from '@/components/billing/billing-modal';
|
||||||
|
|
||||||
export interface ChatInputHandles {
|
export interface ChatInputHandles {
|
||||||
getPendingFiles: () => File[];
|
getPendingFiles: () => File[];
|
||||||
|
@ -64,6 +65,7 @@ export interface ChatInputProps {
|
||||||
onConfigureAgent?: (agentId: string) => void;
|
onConfigureAgent?: (agentId: string) => void;
|
||||||
hideAgentSelection?: boolean;
|
hideAgentSelection?: boolean;
|
||||||
defaultShowSnackbar?: 'tokens' | 'upgrade' | false;
|
defaultShowSnackbar?: 'tokens' | 'upgrade' | false;
|
||||||
|
showToLowCreditUsers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadedFile {
|
export interface UploadedFile {
|
||||||
|
@ -105,6 +107,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
onConfigureAgent,
|
onConfigureAgent,
|
||||||
hideAgentSelection = false,
|
hideAgentSelection = false,
|
||||||
defaultShowSnackbar = false,
|
defaultShowSnackbar = false,
|
||||||
|
showToLowCreditUsers = true,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
@ -122,6 +125,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
const [configModalTab, setConfigModalTab] = useState('integrations');
|
const [configModalTab, setConfigModalTab] = useState('integrations');
|
||||||
const [registryDialogOpen, setRegistryDialogOpen] = useState(false);
|
const [registryDialogOpen, setRegistryDialogOpen] = useState(false);
|
||||||
const [showSnackbar, setShowSnackbar] = useState(defaultShowSnackbar);
|
const [showSnackbar, setShowSnackbar] = useState(defaultShowSnackbar);
|
||||||
|
const [billingModalOpen, setBillingModalOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedModel,
|
selectedModel,
|
||||||
|
@ -133,19 +137,34 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
refreshCustomModels,
|
refreshCustomModels,
|
||||||
} = useModelSelection();
|
} = useModelSelection();
|
||||||
|
|
||||||
const { data: subscriptionData } = useSubscription();
|
const { data: subscriptionData } = useSubscriptionWithStreaming(isAgentRunning);
|
||||||
const deleteFileMutation = useFileDelete();
|
const deleteFileMutation = useFileDelete();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
// Simple logic: show usage preview if we have subscription data and not in local mode
|
// Show usage preview logic:
|
||||||
const shouldShowUsage = !isLocalMode() && subscriptionData;
|
// - 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
|
// Auto-show usage preview when we have subscription data
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (shouldShowUsage && defaultShowSnackbar !== false && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) {
|
if (shouldShowUsage && defaultShowSnackbar !== false && (showSnackbar === false || showSnackbar === defaultShowSnackbar)) {
|
||||||
setShowSnackbar('upgrade');
|
setShowSnackbar('upgrade');
|
||||||
}
|
}
|
||||||
}, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage]);
|
}, [subscriptionData, showSnackbar, defaultShowSnackbar, shouldShowUsage, subscriptionStatus, showToLowCreditUsers]);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
|
@ -320,6 +339,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
showUsagePreview={showSnackbar}
|
showUsagePreview={showSnackbar}
|
||||||
subscriptionData={subscriptionData}
|
subscriptionData={subscriptionData}
|
||||||
onCloseUsage={() => setShowSnackbar(false)}
|
onCloseUsage={() => setShowSnackbar(false)}
|
||||||
|
onOpenUpgrade={() => setBillingModalOpen(true)}
|
||||||
isVisible={showToolPreview || !!showSnackbar}
|
isVisible={showToolPreview || !!showSnackbar}
|
||||||
/>
|
/>
|
||||||
<Card
|
<Card
|
||||||
|
@ -494,6 +514,10 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
/>
|
/>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
<BillingModal
|
||||||
|
open={billingModalOpen}
|
||||||
|
onOpenChange={setBillingModalOpen}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -4,6 +4,7 @@ import { X } from 'lucide-react';
|
||||||
import { cn } from '@/lib/utils';
|
import { cn } from '@/lib/utils';
|
||||||
import { UsagePreview } from './usage-preview';
|
import { UsagePreview } from './usage-preview';
|
||||||
import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview';
|
import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview';
|
||||||
|
import { isLocalMode } from '@/lib/config';
|
||||||
|
|
||||||
export interface ChatSnackProps {
|
export interface ChatSnackProps {
|
||||||
// Tool preview props
|
// Tool preview props
|
||||||
|
@ -17,6 +18,7 @@ export interface ChatSnackProps {
|
||||||
showUsagePreview?: 'tokens' | 'upgrade' | false;
|
showUsagePreview?: 'tokens' | 'upgrade' | false;
|
||||||
subscriptionData?: any;
|
subscriptionData?: any;
|
||||||
onCloseUsage?: () => void;
|
onCloseUsage?: () => void;
|
||||||
|
onOpenUpgrade?: () => void;
|
||||||
|
|
||||||
// General props
|
// General props
|
||||||
isVisible?: boolean;
|
isVisible?: boolean;
|
||||||
|
@ -34,16 +36,21 @@ export const ChatSnack: React.FC<ChatSnackProps> = ({
|
||||||
showUsagePreview = false,
|
showUsagePreview = false,
|
||||||
subscriptionData,
|
subscriptionData,
|
||||||
onCloseUsage,
|
onCloseUsage,
|
||||||
|
onOpenUpgrade,
|
||||||
isVisible = false,
|
isVisible = false,
|
||||||
}) => {
|
}) => {
|
||||||
const [currentView, setCurrentView] = React.useState(0);
|
const [currentView, setCurrentView] = React.useState(0);
|
||||||
|
|
||||||
// Determine what notifications we have
|
// Determine what notifications we have - match exact rendering conditions
|
||||||
const notifications = [];
|
const notifications = [];
|
||||||
|
|
||||||
|
// Tool notification: only if we have tool calls and showToolPreview is true
|
||||||
if (showToolPreview && toolCalls.length > 0) {
|
if (showToolPreview && toolCalls.length > 0) {
|
||||||
notifications.push('tool');
|
notifications.push('tool');
|
||||||
}
|
}
|
||||||
if (showUsagePreview) {
|
|
||||||
|
// Usage notification: must match ALL rendering conditions
|
||||||
|
if (showUsagePreview && !isLocalMode() && subscriptionData) {
|
||||||
notifications.push('usage');
|
notifications.push('usage');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,7 +96,7 @@ export const ChatSnack: React.FC<ChatSnackProps> = ({
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (currentNotification === 'usage' && showUsagePreview) {
|
if (currentNotification === 'usage' && showUsagePreview && !isLocalMode()) {
|
||||||
return (
|
return (
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId={SNACK_LAYOUT_ID}
|
layoutId={SNACK_LAYOUT_ID}
|
||||||
|
@ -106,7 +113,22 @@ export const ChatSnack: React.FC<ChatSnackProps> = ({
|
||||||
>
|
>
|
||||||
<motion.div
|
<motion.div
|
||||||
layoutId={SNACK_CONTENT_LAYOUT_ID}
|
layoutId={SNACK_CONTENT_LAYOUT_ID}
|
||||||
className="bg-card border border-border rounded-3xl p-2 w-full"
|
className={cn(
|
||||||
|
"bg-card border border-border rounded-3xl p-2 w-full transition-all duration-200",
|
||||||
|
onOpenUpgrade && "cursor-pointer hover:shadow-md"
|
||||||
|
)}
|
||||||
|
whileHover={onOpenUpgrade ? { scale: 1.02 } : undefined}
|
||||||
|
whileTap={onOpenUpgrade ? { scale: 0.98 } : undefined}
|
||||||
|
onClick={(e) => {
|
||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
<UsagePreview
|
<UsagePreview
|
||||||
type={showUsagePreview}
|
type={showUsagePreview}
|
||||||
|
@ -126,6 +148,7 @@ export const ChatSnack: React.FC<ChatSnackProps> = ({
|
||||||
currentIndex={currentView}
|
currentIndex={currentView}
|
||||||
totalCount={totalNotifications}
|
totalCount={totalNotifications}
|
||||||
onIndicatorClick={(index) => setCurrentView(index)}
|
onIndicatorClick={(index) => setCurrentView(index)}
|
||||||
|
onOpenUpgrade={onOpenUpgrade}
|
||||||
/>
|
/>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
|
@ -277,13 +277,13 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{subscriptionStatus === 'no_subscription' && !isLocalMode() &&
|
{/* {subscriptionStatus === 'no_subscription' && !isLocalMode() &&
|
||||||
<div className='sm:hidden absolute -bottom-8 left-0 right-0 flex justify-center'>
|
<div className='sm:hidden absolute -bottom-8 left-0 right-0 flex justify-center'>
|
||||||
<p className='text-xs text-amber-500 px-2 py-1'>
|
<p className='text-xs text-amber-500 px-2 py-1'>
|
||||||
Upgrade for better performance
|
Upgrade for better performance
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
}
|
} */}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -9,6 +9,7 @@ export interface UsagePreviewProps {
|
||||||
type: 'tokens' | 'upgrade';
|
type: 'tokens' | 'upgrade';
|
||||||
subscriptionData?: any;
|
subscriptionData?: any;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
|
onOpenUpgrade?: () => void;
|
||||||
hasMultiple?: boolean;
|
hasMultiple?: boolean;
|
||||||
showIndicators?: boolean;
|
showIndicators?: boolean;
|
||||||
currentIndex?: number;
|
currentIndex?: number;
|
||||||
|
@ -20,6 +21,7 @@ export const UsagePreview: React.FC<UsagePreviewProps> = ({
|
||||||
type,
|
type,
|
||||||
subscriptionData,
|
subscriptionData,
|
||||||
onClose,
|
onClose,
|
||||||
|
onOpenUpgrade,
|
||||||
hasMultiple = false,
|
hasMultiple = false,
|
||||||
showIndicators = false,
|
showIndicators = false,
|
||||||
currentIndex = 0,
|
currentIndex = 0,
|
||||||
|
@ -100,6 +102,7 @@ export const UsagePreview: React.FC<UsagePreviewProps> = ({
|
||||||
{/* Apple-style notification indicators - only for multiple notification types */}
|
{/* Apple-style notification indicators - only for multiple notification types */}
|
||||||
{showIndicators && totalCount === 2 && (
|
{showIndicators && totalCount === 2 && (
|
||||||
<button
|
<button
|
||||||
|
data-indicator-click
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
const nextIndex = currentIndex === 0 ? 1 : 0;
|
const nextIndex = currentIndex === 0 ? 1 : 0;
|
||||||
|
@ -121,7 +124,7 @@ export const UsagePreview: React.FC<UsagePreviewProps> = ({
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button value='ghost' className="bg-transparent hover:bg-transparent flex-shrink-0" onClick={onClose}>
|
<Button value='ghost' data-close-click className="bg-transparent hover:bg-transparent flex-shrink-0" onClick={onClose}>
|
||||||
<X className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
<X className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
SubscriptionStatus,
|
SubscriptionStatus,
|
||||||
} from '@/lib/api';
|
} from '@/lib/api';
|
||||||
import { subscriptionKeys } from './keys';
|
import { subscriptionKeys } from './keys';
|
||||||
|
import { useQuery } from '@tanstack/react-query';
|
||||||
|
import { useState, useEffect } from 'react';
|
||||||
|
|
||||||
export const useSubscription = createQueryHook(
|
export const useSubscription = createQueryHook(
|
||||||
subscriptionKeys.details(),
|
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(
|
export const useCreatePortalSession = createMutationHook(
|
||||||
(params: { return_url: string }) => createPortalSession(params),
|
(params: { return_url: string }) => createPortalSession(params),
|
||||||
{
|
{
|
||||||
|
|
Loading…
Reference in New Issue