mirror of https://github.com/kortix-ai/suna.git
commit
7bd68f1e89
|
@ -619,7 +619,7 @@ export default function ThreadPage({
|
||||||
<WorkflowInfo workflowId={workflowId} />
|
<WorkflowInfo workflowId={workflowId} />
|
||||||
</div>
|
</div>
|
||||||
)} */}
|
)} */}
|
||||||
|
|
||||||
<ThreadContent
|
<ThreadContent
|
||||||
messages={messages}
|
messages={messages}
|
||||||
streamingTextContent={streamingTextContent}
|
streamingTextContent={streamingTextContent}
|
||||||
|
@ -670,6 +670,7 @@ export default function ThreadPage({
|
||||||
setIsSidePanelOpen(true);
|
setIsSidePanelOpen(true);
|
||||||
userClosedPanelRef.current = false;
|
userClosedPanelRef.current = false;
|
||||||
}}
|
}}
|
||||||
|
defaultShowSnackbar="tokens"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -7,6 +7,8 @@ import React, {
|
||||||
forwardRef,
|
forwardRef,
|
||||||
useImperativeHandle,
|
useImperativeHandle,
|
||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
|
import { motion } from 'framer-motion';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { handleFiles } from './file-upload-handler';
|
import { handleFiles } from './file-upload-handler';
|
||||||
import { MessageInput } from './message-input';
|
import { MessageInput } from './message-input';
|
||||||
|
@ -14,13 +16,19 @@ import { AttachmentGroup } from '../attachment-group';
|
||||||
import { useModelSelection } from './_use-model-selection';
|
import { useModelSelection } from './_use-model-selection';
|
||||||
import { useFileDelete } from '@/hooks/react-query/files';
|
import { useFileDelete } from '@/hooks/react-query/files';
|
||||||
import { useQueryClient } from '@tanstack/react-query';
|
import { useQueryClient } from '@tanstack/react-query';
|
||||||
import { FloatingToolPreview, ToolCallInput } from './floating-tool-preview';
|
import { ToolCallInput } from './floating-tool-preview';
|
||||||
import { Settings2, Sparkles, Brain, ChevronRight, Zap, Workflow, Database, Wrench } from 'lucide-react';
|
import { ChatSnack } from './chat-snack';
|
||||||
|
import { Brain, ChevronRight, Zap, Workflow, Database, Wrench, X } from 'lucide-react';
|
||||||
import { FaGoogle, FaDiscord } from 'react-icons/fa';
|
import { FaGoogle, FaDiscord } from 'react-icons/fa';
|
||||||
import { SiNotion } from 'react-icons/si';
|
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 { 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 {
|
export interface ChatInputHandles {
|
||||||
getPendingFiles: () => File[];
|
getPendingFiles: () => File[];
|
||||||
|
@ -56,6 +64,8 @@ export interface ChatInputProps {
|
||||||
enableAdvancedConfig?: boolean;
|
enableAdvancedConfig?: boolean;
|
||||||
onConfigureAgent?: (agentId: string) => void;
|
onConfigureAgent?: (agentId: string) => void;
|
||||||
hideAgentSelection?: boolean;
|
hideAgentSelection?: boolean;
|
||||||
|
defaultShowSnackbar?: 'tokens' | 'upgrade' | false;
|
||||||
|
showToLowCreditUsers?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface UploadedFile {
|
export interface UploadedFile {
|
||||||
|
@ -66,6 +76,8 @@ export interface UploadedFile {
|
||||||
localUrl?: string;
|
localUrl?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
@ -94,6 +106,8 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
enableAdvancedConfig = false,
|
enableAdvancedConfig = false,
|
||||||
onConfigureAgent,
|
onConfigureAgent,
|
||||||
hideAgentSelection = false,
|
hideAgentSelection = false,
|
||||||
|
defaultShowSnackbar = false,
|
||||||
|
showToLowCreditUsers = true,
|
||||||
},
|
},
|
||||||
ref,
|
ref,
|
||||||
) => {
|
) => {
|
||||||
|
@ -110,6 +124,8 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
const [configModalOpen, setConfigModalOpen] = useState(false);
|
const [configModalOpen, setConfigModalOpen] = useState(false);
|
||||||
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 [billingModalOpen, setBillingModalOpen] = useState(false);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
selectedModel,
|
selectedModel,
|
||||||
|
@ -121,9 +137,35 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
refreshCustomModels,
|
refreshCustomModels,
|
||||||
} = useModelSelection();
|
} = useModelSelection();
|
||||||
|
|
||||||
|
const { data: subscriptionData } = useSubscriptionWithStreaming(isAgentRunning);
|
||||||
const deleteFileMutation = useFileDelete();
|
const deleteFileMutation = useFileDelete();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
// 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, subscriptionStatus, showToLowCreditUsers]);
|
||||||
|
|
||||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||||
const fileInputRef = useRef<HTMLInputElement>(null);
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
||||||
const hasLoadedFromLocalStorage = useRef(false);
|
const hasLoadedFromLocalStorage = useRef(false);
|
||||||
|
@ -137,7 +179,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
if (typeof window !== 'undefined' && onAgentSelect && !hasLoadedFromLocalStorage.current) {
|
if (typeof window !== 'undefined' && onAgentSelect && !hasLoadedFromLocalStorage.current) {
|
||||||
const urlParams = new URLSearchParams(window.location.search);
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
const hasAgentIdInUrl = urlParams.has('agent_id');
|
const hasAgentIdInUrl = urlParams.has('agent_id');
|
||||||
|
|
||||||
if (!selectedAgentId && !hasAgentIdInUrl) {
|
if (!selectedAgentId && !hasAgentIdInUrl) {
|
||||||
const savedAgentId = localStorage.getItem('lastSelectedAgentId');
|
const savedAgentId = localStorage.getItem('lastSelectedAgentId');
|
||||||
if (savedAgentId) {
|
if (savedAgentId) {
|
||||||
|
@ -283,185 +325,200 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
||||||
setIsDraggingOver(false);
|
setIsDraggingOver(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mx-auto w-full max-w-4xl">
|
<div className="mx-auto w-full max-w-4xl relative">
|
||||||
<FloatingToolPreview
|
<div className="relative">
|
||||||
toolCalls={toolCalls}
|
<ChatSnack
|
||||||
currentIndex={toolCallIndex}
|
toolCalls={toolCalls}
|
||||||
onExpand={onExpandToolPreview || (() => { })}
|
toolCallIndex={toolCallIndex}
|
||||||
agentName={agentName}
|
onExpandToolPreview={onExpandToolPreview}
|
||||||
isVisible={showToolPreview}
|
agentName={agentName}
|
||||||
/>
|
showToolPreview={showToolPreview}
|
||||||
<Card
|
showUsagePreview={showSnackbar}
|
||||||
className={`-mb-2 shadow-none w-full max-w-4xl mx-auto bg-transparent border-none overflow-hidden ${enableAdvancedConfig && selectedAgentId ? '' : 'rounded-3xl'}`}
|
subscriptionData={subscriptionData}
|
||||||
onDragOver={handleDragOver}
|
onCloseUsage={() => setShowSnackbar(false)}
|
||||||
onDragLeave={handleDragLeave}
|
onOpenUpgrade={() => setBillingModalOpen(true)}
|
||||||
onDrop={(e) => {
|
isVisible={showToolPreview || !!showSnackbar}
|
||||||
e.preventDefault();
|
/>
|
||||||
e.stopPropagation();
|
<Card
|
||||||
setIsDraggingOver(false);
|
className={`-mb-2 shadow-none w-full max-w-4xl mx-auto bg-transparent border-none overflow-visible ${enableAdvancedConfig && selectedAgentId ? '' : 'rounded-3xl'} relative`}
|
||||||
if (fileInputRef.current && e.dataTransfer.files.length > 0) {
|
onDragOver={handleDragOver}
|
||||||
const files = Array.from(e.dataTransfer.files);
|
onDragLeave={handleDragLeave}
|
||||||
handleFiles(
|
onDrop={(e) => {
|
||||||
files,
|
e.preventDefault();
|
||||||
sandboxId,
|
e.stopPropagation();
|
||||||
setPendingFiles,
|
setIsDraggingOver(false);
|
||||||
setUploadedFiles,
|
if (fileInputRef.current && e.dataTransfer.files.length > 0) {
|
||||||
setIsUploading,
|
const files = Array.from(e.dataTransfer.files);
|
||||||
messages,
|
handleFiles(
|
||||||
queryClient,
|
files,
|
||||||
);
|
sandboxId,
|
||||||
}
|
setPendingFiles,
|
||||||
}}
|
setUploadedFiles,
|
||||||
>
|
setIsUploading,
|
||||||
<div className="w-full text-sm flex flex-col justify-between items-start rounded-lg">
|
messages,
|
||||||
<CardContent className={`w-full p-1.5 ${enableAdvancedConfig && selectedAgentId ? 'pb-1' : 'pb-2'} ${bgColor} border ${enableAdvancedConfig && selectedAgentId ? 'rounded-t-3xl' : 'rounded-3xl'}`}>
|
queryClient,
|
||||||
<AttachmentGroup
|
);
|
||||||
files={uploadedFiles || []}
|
}
|
||||||
sandboxId={sandboxId}
|
}}
|
||||||
onRemove={removeUploadedFile}
|
>
|
||||||
layout="inline"
|
|
||||||
maxHeight="216px"
|
|
||||||
showPreviews={true}
|
|
||||||
/>
|
|
||||||
<MessageInput
|
|
||||||
ref={textareaRef}
|
|
||||||
value={value}
|
|
||||||
onChange={handleChange}
|
|
||||||
onSubmit={handleSubmit}
|
|
||||||
onTranscription={handleTranscription}
|
|
||||||
placeholder={placeholder}
|
|
||||||
loading={loading}
|
|
||||||
disabled={disabled}
|
|
||||||
isAgentRunning={isAgentRunning}
|
|
||||||
onStopAgent={onStopAgent}
|
|
||||||
isDraggingOver={isDraggingOver}
|
|
||||||
uploadedFiles={uploadedFiles}
|
|
||||||
|
|
||||||
fileInputRef={fileInputRef}
|
|
||||||
isUploading={isUploading}
|
|
||||||
sandboxId={sandboxId}
|
|
||||||
setPendingFiles={setPendingFiles}
|
|
||||||
setUploadedFiles={setUploadedFiles}
|
|
||||||
setIsUploading={setIsUploading}
|
|
||||||
hideAttachments={hideAttachments}
|
|
||||||
messages={messages}
|
|
||||||
|
|
||||||
selectedModel={selectedModel}
|
<div className="w-full text-sm flex flex-col justify-between items-start rounded-lg">
|
||||||
onModelChange={handleModelChange}
|
<CardContent className={`w-full p-1.5 ${enableAdvancedConfig && selectedAgentId ? 'pb-1' : 'pb-2'} ${bgColor} border ${enableAdvancedConfig && selectedAgentId ? 'rounded-t-3xl' : 'rounded-3xl'}`}>
|
||||||
modelOptions={modelOptions}
|
<AttachmentGroup
|
||||||
subscriptionStatus={subscriptionStatus}
|
files={uploadedFiles || []}
|
||||||
canAccessModel={canAccessModel}
|
sandboxId={sandboxId}
|
||||||
refreshCustomModels={refreshCustomModels}
|
onRemove={removeUploadedFile}
|
||||||
isLoggedIn={isLoggedIn}
|
layout="inline"
|
||||||
|
maxHeight="216px"
|
||||||
|
showPreviews={true}
|
||||||
|
/>
|
||||||
|
<MessageInput
|
||||||
|
ref={textareaRef}
|
||||||
|
value={value}
|
||||||
|
onChange={handleChange}
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
onTranscription={handleTranscription}
|
||||||
|
placeholder={placeholder}
|
||||||
|
loading={loading}
|
||||||
|
disabled={disabled}
|
||||||
|
isAgentRunning={isAgentRunning}
|
||||||
|
onStopAgent={onStopAgent}
|
||||||
|
isDraggingOver={isDraggingOver}
|
||||||
|
uploadedFiles={uploadedFiles}
|
||||||
|
|
||||||
selectedAgentId={selectedAgentId}
|
fileInputRef={fileInputRef}
|
||||||
onAgentSelect={onAgentSelect}
|
isUploading={isUploading}
|
||||||
hideAgentSelection={hideAgentSelection}
|
sandboxId={sandboxId}
|
||||||
/>
|
setPendingFiles={setPendingFiles}
|
||||||
</CardContent>
|
setUploadedFiles={setUploadedFiles}
|
||||||
|
setIsUploading={setIsUploading}
|
||||||
{enableAdvancedConfig && selectedAgentId && (
|
hideAttachments={hideAttachments}
|
||||||
<div className="w-full border-t border-border/30 bg-muted/20 px-4 py-1.5 rounded-b-3xl border-l border-r border-b border-border">
|
messages={messages}
|
||||||
<div className="flex items-center justify-between">
|
|
||||||
<div className="flex items-center gap-1 sm:gap-2 overflow-x-auto scrollbar-none">
|
selectedModel={selectedModel}
|
||||||
<button
|
onModelChange={handleModelChange}
|
||||||
onClick={() => setRegistryDialogOpen(true)}
|
modelOptions={modelOptions}
|
||||||
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
subscriptionStatus={subscriptionStatus}
|
||||||
>
|
canAccessModel={canAccessModel}
|
||||||
<div className="flex items-center -space-x-0.5">
|
refreshCustomModels={refreshCustomModels}
|
||||||
<div className="w-5 h-5 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
|
isLoggedIn={isLoggedIn}
|
||||||
<FaGoogle className="w-3 h-3" />
|
|
||||||
|
selectedAgentId={selectedAgentId}
|
||||||
|
onAgentSelect={onAgentSelect}
|
||||||
|
hideAgentSelection={hideAgentSelection}
|
||||||
|
/>
|
||||||
|
</CardContent>
|
||||||
|
|
||||||
|
{enableAdvancedConfig && selectedAgentId && (
|
||||||
|
<div className="w-full border-t border-border/30 bg-muted/20 px-4 py-1.5 rounded-b-3xl border-l border-r border-b border-border">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<div className="flex items-center gap-1 sm:gap-2 overflow-x-auto scrollbar-none">
|
||||||
|
<button
|
||||||
|
onClick={() => setRegistryDialogOpen(true)}
|
||||||
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
||||||
|
>
|
||||||
|
<div className="flex items-center -space-x-0.5">
|
||||||
|
<div className="w-5 h-5 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
|
||||||
|
<FaGoogle className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
<div className="w-5 h-5 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
|
||||||
|
<FaDiscord className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
|
<div className="w-5 h-5 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
|
||||||
|
<SiNotion className="w-3 h-3" />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-5 h-5 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
|
<span className="text-xs font-medium">Integrations</span>
|
||||||
<FaDiscord className="w-3 h-3" />
|
</button>
|
||||||
</div>
|
|
||||||
<div className="w-5 h-5 bg-white dark:bg-muted border border-border rounded-full flex items-center justify-center shadow-sm">
|
<div className="w-px h-4 bg-border/60" />
|
||||||
<SiNotion className="w-3 h-3" />
|
|
||||||
</div>
|
<button
|
||||||
</div>
|
onClick={() => {
|
||||||
<span className="text-xs font-medium">Integrations</span>
|
setConfigModalTab('instructions');
|
||||||
</button>
|
setConfigModalOpen(true);
|
||||||
|
}}
|
||||||
<div className="w-px h-4 bg-border/60" />
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
||||||
|
>
|
||||||
<button
|
<Brain className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
onClick={() => {
|
<span className="text-xs font-medium">Instructions</span>
|
||||||
setConfigModalTab('instructions');
|
</button>
|
||||||
setConfigModalOpen(true);
|
|
||||||
}}
|
<div className="w-px h-4 bg-border/60" />
|
||||||
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
|
||||||
>
|
<button
|
||||||
<Brain className="h-3.5 w-3.5 flex-shrink-0" />
|
onClick={() => {
|
||||||
<span className="text-xs font-medium">Instructions</span>
|
setConfigModalTab('knowledge');
|
||||||
</button>
|
setConfigModalOpen(true);
|
||||||
|
}}
|
||||||
<div className="w-px h-4 bg-border/60" />
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
||||||
|
>
|
||||||
<button
|
<Database className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
onClick={() => {
|
<span className="text-xs font-medium">Knowledge</span>
|
||||||
setConfigModalTab('knowledge');
|
</button>
|
||||||
setConfigModalOpen(true);
|
|
||||||
}}
|
<div className="w-px h-4 bg-border/60" />
|
||||||
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
|
||||||
>
|
<button
|
||||||
<Database className="h-3.5 w-3.5 flex-shrink-0" />
|
onClick={() => {
|
||||||
<span className="text-xs font-medium">Knowledge</span>
|
setConfigModalTab('triggers');
|
||||||
</button>
|
setConfigModalOpen(true);
|
||||||
|
}}
|
||||||
<div className="w-px h-4 bg-border/60" />
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
||||||
|
>
|
||||||
<button
|
<Zap className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
onClick={() => {
|
<span className="text-xs font-medium">Triggers</span>
|
||||||
setConfigModalTab('triggers');
|
</button>
|
||||||
setConfigModalOpen(true);
|
|
||||||
}}
|
<div className="w-px h-4 bg-border/60" />
|
||||||
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
|
||||||
>
|
<button
|
||||||
<Zap className="h-3.5 w-3.5 flex-shrink-0" />
|
onClick={() => {
|
||||||
<span className="text-xs font-medium">Triggers</span>
|
setConfigModalTab('workflows');
|
||||||
</button>
|
setConfigModalOpen(true);
|
||||||
|
}}
|
||||||
<div className="w-px h-4 bg-border/60" />
|
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
||||||
|
>
|
||||||
<button
|
<Workflow className="h-3.5 w-3.5 flex-shrink-0" />
|
||||||
onClick={() => {
|
<span className="text-xs font-medium">Workflows</span>
|
||||||
setConfigModalTab('workflows');
|
</button>
|
||||||
setConfigModalOpen(true);
|
</div>
|
||||||
}}
|
|
||||||
className="flex items-center gap-1.5 text-muted-foreground hover:text-foreground transition-all duration-200 px-2.5 py-1.5 rounded-md hover:bg-muted/50 border border-transparent hover:border-border/30 flex-shrink-0"
|
|
||||||
>
|
|
||||||
<Workflow className="h-3.5 w-3.5 flex-shrink-0" />
|
|
||||||
<span className="text-xs font-medium">Workflows</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
</div>
|
||||||
</div>
|
</Card>
|
||||||
</Card>
|
<AgentConfigModal
|
||||||
<AgentConfigModal
|
isOpen={configModalOpen}
|
||||||
isOpen={configModalOpen}
|
onOpenChange={setConfigModalOpen}
|
||||||
onOpenChange={setConfigModalOpen}
|
selectedAgentId={selectedAgentId}
|
||||||
selectedAgentId={selectedAgentId}
|
onAgentSelect={onAgentSelect}
|
||||||
onAgentSelect={onAgentSelect}
|
initialTab={configModalTab}
|
||||||
initialTab={configModalTab}
|
/>
|
||||||
/>
|
<Dialog open={registryDialogOpen} onOpenChange={setRegistryDialogOpen}>
|
||||||
<Dialog open={registryDialogOpen} onOpenChange={setRegistryDialogOpen}>
|
<DialogContent className="p-0 max-w-6xl max-h-[90vh] overflow-y-auto">
|
||||||
<DialogContent className="p-0 max-w-6xl max-h-[90vh] overflow-y-auto">
|
<DialogHeader className="sr-only">
|
||||||
<DialogHeader className="sr-only">
|
<DialogTitle>Integrations</DialogTitle>
|
||||||
<DialogTitle>Integrations</DialogTitle>
|
</DialogHeader>
|
||||||
</DialogHeader>
|
<PipedreamRegistry
|
||||||
<PipedreamRegistry
|
showAgentSelector={true}
|
||||||
showAgentSelector={true}
|
selectedAgentId={selectedAgentId}
|
||||||
selectedAgentId={selectedAgentId}
|
onAgentChange={onAgentSelect}
|
||||||
onAgentChange={onAgentSelect}
|
onToolsSelected={(profileId, selectedTools, appName, appSlug) => {
|
||||||
onToolsSelected={(profileId, selectedTools, appName, appSlug) => {
|
console.log('Tools selected:', { profileId, selectedTools, appName, appSlug });
|
||||||
console.log('Tools selected:', { profileId, selectedTools, appName, appSlug });
|
}}
|
||||||
}}
|
/>
|
||||||
/>
|
</DialogContent>
|
||||||
</DialogContent>
|
</Dialog>
|
||||||
</Dialog>
|
<BillingModal
|
||||||
|
open={billingModalOpen}
|
||||||
|
onOpenChange={setBillingModalOpen}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,176 @@
|
||||||
|
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';
|
||||||
|
import { isLocalMode } from '@/lib/config';
|
||||||
|
|
||||||
|
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;
|
||||||
|
onOpenUpgrade?: () => 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<ChatSnackProps> = ({
|
||||||
|
toolCalls = [],
|
||||||
|
toolCallIndex = 0,
|
||||||
|
onExpandToolPreview,
|
||||||
|
agentName,
|
||||||
|
showToolPreview = false,
|
||||||
|
showUsagePreview = false,
|
||||||
|
subscriptionData,
|
||||||
|
onCloseUsage,
|
||||||
|
onOpenUpgrade,
|
||||||
|
isVisible = false,
|
||||||
|
}) => {
|
||||||
|
const [currentView, setCurrentView] = React.useState(0);
|
||||||
|
|
||||||
|
// 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');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Usage notification: must match ALL rendering conditions
|
||||||
|
if (showUsagePreview && !isLocalMode() && subscriptionData) {
|
||||||
|
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 (
|
||||||
|
<FloatingToolPreview
|
||||||
|
toolCalls={toolCalls}
|
||||||
|
currentIndex={toolCallIndex}
|
||||||
|
onExpand={onExpandToolPreview || (() => { })}
|
||||||
|
agentName={agentName}
|
||||||
|
isVisible={true}
|
||||||
|
showIndicators={hasMultiple}
|
||||||
|
indicatorIndex={currentView}
|
||||||
|
indicatorTotal={totalNotifications}
|
||||||
|
onIndicatorClick={(index) => setCurrentView(index)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentNotification === 'usage' && showUsagePreview && !isLocalMode()) {
|
||||||
|
return (
|
||||||
|
<motion.div
|
||||||
|
layoutId={SNACK_LAYOUT_ID}
|
||||||
|
layout
|
||||||
|
transition={{
|
||||||
|
layout: {
|
||||||
|
type: "spring",
|
||||||
|
stiffness: 300,
|
||||||
|
damping: 30
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
className="-mb-4 w-full"
|
||||||
|
style={{ pointerEvents: 'auto' }}
|
||||||
|
>
|
||||||
|
<motion.div
|
||||||
|
layoutId={SNACK_CONTENT_LAYOUT_ID}
|
||||||
|
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
|
||||||
|
type={showUsagePreview}
|
||||||
|
subscriptionData={subscriptionData}
|
||||||
|
onClose={() => {
|
||||||
|
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)}
|
||||||
|
onOpenUpgrade={onOpenUpgrade}
|
||||||
|
/>
|
||||||
|
</motion.div>
|
||||||
|
</motion.div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AnimatePresence mode="wait">
|
||||||
|
{isVisible && (
|
||||||
|
<motion.div
|
||||||
|
key={currentNotification}
|
||||||
|
initial={{ opacity: 0, y: 8 }}
|
||||||
|
animate={{ opacity: 1, y: 0 }}
|
||||||
|
exit={{ opacity: 0, y: 8 }}
|
||||||
|
transition={{ duration: 0.3, ease: 'easeOut' }}
|
||||||
|
>
|
||||||
|
{renderContent()}
|
||||||
|
</motion.div>
|
||||||
|
)}
|
||||||
|
</AnimatePresence>
|
||||||
|
);
|
||||||
|
};
|
|
@ -25,6 +25,11 @@ interface FloatingToolPreviewProps {
|
||||||
onExpand: () => void;
|
onExpand: () => void;
|
||||||
agentName?: string;
|
agentName?: string;
|
||||||
isVisible: boolean;
|
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';
|
const FLOATING_LAYOUT_ID = 'tool-panel-float';
|
||||||
|
@ -61,6 +66,10 @@ export const FloatingToolPreview: React.FC<FloatingToolPreviewProps> = ({
|
||||||
onExpand,
|
onExpand,
|
||||||
agentName,
|
agentName,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
showIndicators = false,
|
||||||
|
indicatorIndex = 0,
|
||||||
|
indicatorTotal = 1,
|
||||||
|
onIndicatorClick,
|
||||||
}) => {
|
}) => {
|
||||||
const [isExpanding, setIsExpanding] = React.useState(false);
|
const [isExpanding, setIsExpanding] = React.useState(false);
|
||||||
const currentToolCall = toolCalls[currentIndex];
|
const currentToolCall = toolCalls[currentIndex];
|
||||||
|
@ -137,11 +146,6 @@ export const FloatingToolPreview: React.FC<FloatingToolPreviewProps> = ({
|
||||||
<h4 className="text-sm font-medium text-foreground truncate">
|
<h4 className="text-sm font-medium text-foreground truncate">
|
||||||
{getUserFriendlyToolName(toolName)}
|
{getUserFriendlyToolName(toolName)}
|
||||||
</h4>
|
</h4>
|
||||||
<div className="flex items-center gap-1">
|
|
||||||
<span className="text-xs text-muted-foreground">
|
|
||||||
{currentIndex + 1}/{totalCalls}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</motion.div>
|
</motion.div>
|
||||||
|
|
||||||
<motion.div layoutId="tool-status" className="flex items-center gap-2">
|
<motion.div layoutId="tool-status" className="flex items-center gap-2">
|
||||||
|
@ -164,6 +168,32 @@ export const FloatingToolPreview: React.FC<FloatingToolPreviewProps> = ({
|
||||||
</motion.div>
|
</motion.div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Apple-style notification indicators - only for multiple notification types */}
|
||||||
|
{showIndicators && indicatorTotal === 2 && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation(); // Prevent tool expansion
|
||||||
|
// Toggle between the two notifications (binary switch)
|
||||||
|
const nextIndex = indicatorIndex === 0 ? 1 : 0;
|
||||||
|
onIndicatorClick?.(nextIndex);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 mr-3 px-2 py-1.5 rounded-lg hover:bg-muted/30 transition-colors"
|
||||||
|
style={{ opacity: isExpanding ? 0 : 1 }}
|
||||||
|
>
|
||||||
|
{Array.from({ length: indicatorTotal }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-300 ease-out rounded-full",
|
||||||
|
index === indicatorIndex
|
||||||
|
? "w-6 h-2 bg-foreground"
|
||||||
|
: "w-3 h-2 bg-muted-foreground/40"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button value='ghost' className="bg-transparent hover:bg-transparent flex-shrink-0" style={{ opacity: isExpanding ? 0 : 1 }}>
|
<Button value='ghost' className="bg-transparent hover:bg-transparent flex-shrink-0" style={{ opacity: isExpanding ? 0 : 1 }}>
|
||||||
<Maximize2 className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
<Maximize2 className="h-4 w-4 text-muted-foreground group-hover:text-foreground transition-colors" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -156,7 +156,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
||||||
const renderDropdown = () => {
|
const renderDropdown = () => {
|
||||||
if (isLoggedIn) {
|
if (isLoggedIn) {
|
||||||
const showAdvancedFeatures = enableAdvancedConfig || (customAgentsEnabled && !flagsLoading);
|
const showAdvancedFeatures = enableAdvancedConfig || (customAgentsEnabled && !flagsLoading);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{showAdvancedFeatures && !hideAgentSelection && (
|
{showAdvancedFeatures && !hideAgentSelection && (
|
||||||
|
@ -223,7 +223,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{subscriptionStatus === 'no_subscription' && !isLocalMode() &&
|
{/* {subscriptionStatus === 'no_subscription' && !isLocalMode() &&
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger>
|
<TooltipTrigger>
|
||||||
|
@ -234,7 +234,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
}
|
} */}
|
||||||
|
|
||||||
<div className='flex items-center gap-2'>
|
<div className='flex items-center gap-2'>
|
||||||
{renderDropdown()}
|
{renderDropdown()}
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,132 @@
|
||||||
|
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;
|
||||||
|
onOpenUpgrade?: () => void;
|
||||||
|
hasMultiple?: boolean;
|
||||||
|
showIndicators?: boolean;
|
||||||
|
currentIndex?: number;
|
||||||
|
totalCount?: number;
|
||||||
|
onIndicatorClick?: (index: number) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const UsagePreview: React.FC<UsagePreviewProps> = ({
|
||||||
|
type,
|
||||||
|
subscriptionData,
|
||||||
|
onClose,
|
||||||
|
onOpenUpgrade,
|
||||||
|
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 (
|
||||||
|
<div className="flex items-center gap-3">
|
||||||
|
{/* Icon */}
|
||||||
|
<div className="flex-shrink-0">
|
||||||
|
<motion.div
|
||||||
|
className={cn(
|
||||||
|
"w-10 h-10 rounded-2xl flex items-center justify-center",
|
||||||
|
isOverLimit()
|
||||||
|
? "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800"
|
||||||
|
: "bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Zap className={cn(
|
||||||
|
"h-5 w-5",
|
||||||
|
isOverLimit()
|
||||||
|
? "text-red-500 dark:text-red-400"
|
||||||
|
: "text-blue-500 dark:text-blue-400"
|
||||||
|
)} />
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Content */}
|
||||||
|
<div className="flex-1 min-w-0">
|
||||||
|
<motion.div className="flex items-center gap-2 mb-1">
|
||||||
|
<h4 className="text-sm font-medium text-foreground truncate">
|
||||||
|
Upgrade for more usage
|
||||||
|
</h4>
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<motion.div className="flex items-center gap-2">
|
||||||
|
<div className={cn(
|
||||||
|
"w-2 h-2 rounded-full",
|
||||||
|
isOverLimit() ? "bg-red-500" : "bg-blue-500"
|
||||||
|
)} />
|
||||||
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
|
{getUsageDisplay()}
|
||||||
|
</span>
|
||||||
|
</motion.div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Apple-style notification indicators - only for multiple notification types */}
|
||||||
|
{showIndicators && totalCount === 2 && (
|
||||||
|
<button
|
||||||
|
data-indicator-click
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
const nextIndex = currentIndex === 0 ? 1 : 0;
|
||||||
|
onIndicatorClick?.(nextIndex);
|
||||||
|
}}
|
||||||
|
className="flex items-center gap-1.5 mr-3 px-2 py-1.5 rounded-lg hover:bg-muted/30 transition-colors"
|
||||||
|
>
|
||||||
|
{Array.from({ length: totalCount }).map((_, index) => (
|
||||||
|
<div
|
||||||
|
key={index}
|
||||||
|
className={cn(
|
||||||
|
"transition-all duration-300 ease-out rounded-full",
|
||||||
|
index === currentIndex
|
||||||
|
? "w-6 h-2 bg-foreground"
|
||||||
|
: "w-3 h-2 bg-muted-foreground/40"
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<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" />
|
||||||
|
</Button>
|
||||||
|
</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