Merge branch 'main' into pipedream-mcp

This commit is contained in:
Saumya 2025-07-08 18:50:06 +05:30
commit c119b1dc5d
11 changed files with 285 additions and 300 deletions

View File

@ -184,6 +184,9 @@ async def get_agent_run_with_access_check(client, agent_run_id: str, user_id: st
await verify_thread_access(client, thread_id, user_id) await verify_thread_access(client, thread_id, user_id)
return agent_run_data return agent_run_data
@router.post("/thread/{thread_id}/agent/start") @router.post("/thread/{thread_id}/agent/start")
async def start_agent( async def start_agent(
thread_id: str, thread_id: str,

View File

@ -10,30 +10,6 @@ class SandboxExposeTool(SandboxToolsBase):
def __init__(self, project_id: str, thread_manager: ThreadManager): def __init__(self, project_id: str, thread_manager: ThreadManager):
super().__init__(project_id, thread_manager) super().__init__(project_id, thread_manager)
async def _wait_for_sandbox_services(self, timeout: int = 30) -> bool:
"""Wait for sandbox services to be fully started before exposing ports."""
start_time = time.time()
while time.time() - start_time < timeout:
try:
# Check if supervisord is running and managing services
result = await self.sandbox.process.exec("supervisorctl status", timeout=10)
if result.exit_code == 0:
# Check if key services are running
status_output = result.output
if "http_server" in status_output and "RUNNING" in status_output:
return True
# If services aren't ready, wait a bit
await asyncio.sleep(2)
except Exception as e:
# If we can't check status, wait a bit and try again
await asyncio.sleep(2)
return False
@openapi_schema({ @openapi_schema({
"type": "function", "type": "function",
"function": { "function": {
@ -93,11 +69,6 @@ class SandboxExposeTool(SandboxToolsBase):
if not 1 <= port <= 65535: if not 1 <= port <= 65535:
return self.fail_response(f"Invalid port number: {port}. Must be between 1 and 65535.") return self.fail_response(f"Invalid port number: {port}. Must be between 1 and 65535.")
# Wait for sandbox services to be ready (especially important for workflows)
services_ready = await self._wait_for_sandbox_services()
if not services_ready:
return self.fail_response(f"Sandbox services are not fully started yet. Please wait a moment and try again, or ensure a service is running on port {port}.")
# Check if something is actually listening on the port (for custom ports) # Check if something is actually listening on the port (for custom ports)
if port not in [6080, 8080, 8003]: # Skip check for known sandbox ports if port not in [6080, 8080, 8003]: # Skip check for known sandbox ports
try: try:

View File

@ -28,7 +28,7 @@ export default function Home() {
<div className='flex flex-col items-center px-4'> <div className='flex flex-col items-center px-4'>
<PricingSection /> <PricingSection />
</div> </div>
<div className="mt-12 pb-12 mx-auto"> <div className="pb-10 mx-auto">
<HeroVideoSection /> <HeroVideoSection />
</div> </div>
{/* <TestimonialSection /> */} {/* <TestimonialSection /> */}

View File

@ -16,6 +16,7 @@ import { useInitiateAgentMutation } from '@/hooks/react-query/dashboard/use-init
import { useThreadQuery } from '@/hooks/react-query/threads/use-threads'; import { useThreadQuery } from '@/hooks/react-query/threads/use-threads';
import { generateThreadName } from '@/lib/actions/threads'; import { generateThreadName } from '@/lib/actions/threads';
import GoogleSignIn from '@/components/GoogleSignIn'; import GoogleSignIn from '@/components/GoogleSignIn';
import { useAgents } from '@/hooks/react-query/agents/use-agents';
import { import {
Dialog, Dialog,
DialogContent, DialogContent,
@ -30,13 +31,11 @@ import { useAccounts } from '@/hooks/use-accounts';
import { isLocalMode, config } from '@/lib/config'; import { isLocalMode, config } from '@/lib/config';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useModal } from '@/hooks/use-modal-store'; import { useModal } from '@/hooks/use-modal-store';
import { Card, CardContent } from '@/components/ui/card'; import { ChatInput, ChatInputHandles } from '@/components/thread/chat-input/chat-input';
import { Button } from '@/components/ui/button'; import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
import { Send, ArrowUp, Paperclip } from 'lucide-react'; import { createQueryHook } from '@/hooks/use-query';
import { Textarea } from '@/components/ui/textarea'; import { agentKeys } from '@/hooks/react-query/agents/keys';
import { cn } from '@/lib/utils'; import { getAgents } from '@/hooks/react-query/agents/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import ChatDropdown from '@/components/thread/chat-input/chat-dropdown';
// Custom dialog overlay with blur effect // Custom dialog overlay with blur effect
const BlurredDialogOverlay = () => ( const BlurredDialogOverlay = () => (
@ -55,6 +54,7 @@ export function HeroSection() {
const scrollTimeout = useRef<NodeJS.Timeout | null>(null); const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
const { scrollY } = useScroll(); const { scrollY } = useScroll();
const [inputValue, setInputValue] = useState(''); const [inputValue, setInputValue] = useState('');
const [selectedAgentId, setSelectedAgentId] = useState<string | undefined>();
const router = useRouter(); const router = useRouter();
const { user, isLoading } = useAuth(); const { user, isLoading } = useAuth();
const { billingError, handleBillingError, clearBillingError } = const { billingError, handleBillingError, clearBillingError } =
@ -65,6 +65,28 @@ export function HeroSection() {
const initiateAgentMutation = useInitiateAgentMutation(); const initiateAgentMutation = useInitiateAgentMutation();
const [initiatedThreadId, setInitiatedThreadId] = useState<string | null>(null); const [initiatedThreadId, setInitiatedThreadId] = useState<string | null>(null);
const threadQuery = useThreadQuery(initiatedThreadId || ''); const threadQuery = useThreadQuery(initiatedThreadId || '');
const chatInputRef = useRef<ChatInputHandles>(null);
// Fetch agents for selection
const { data: agentsResponse } = createQueryHook(
agentKeys.list({
limit: 100,
sort_by: 'name',
sort_order: 'asc'
}),
() => getAgents({
limit: 100,
sort_by: 'name',
sort_order: 'asc'
}),
{
enabled: !!user && !isLoading,
staleTime: 5 * 60 * 1000,
gcTime: 10 * 60 * 1000,
}
)();
const agents = agentsResponse?.agents || [];
// Auth dialog state // Auth dialog state
const [authDialogOpen, setAuthDialogOpen] = useState(false); const [authDialogOpen, setAuthDialogOpen] = useState(false);
@ -116,31 +138,66 @@ export function HeroSection() {
if (thread.project_id) { if (thread.project_id) {
router.push(`/projects/${thread.project_id}/thread/${initiatedThreadId}`); router.push(`/projects/${thread.project_id}/thread/${initiatedThreadId}`);
} else { } else {
router.push(`/thread/${initiatedThreadId}`); router.push(`/agents/${initiatedThreadId}`);
} }
setInitiatedThreadId(null); setInitiatedThreadId(null);
} }
}, [threadQuery.data, initiatedThreadId, router]); }, [threadQuery.data, initiatedThreadId, router]);
const createAgentWithPrompt = async () => { // Handle ChatInput submission
if (!inputValue.trim() || isSubmitting) return; const handleChatInputSubmit = async (
message: string,
options?: { model_name?: string; enable_thinking?: boolean }
) => {
if ((!message.trim() && !chatInputRef.current?.getPendingFiles().length) || isSubmitting) return;
// If user is not logged in, save prompt and show auth dialog
if (!user && !isLoading) {
localStorage.setItem(PENDING_PROMPT_KEY, message.trim());
setAuthDialogOpen(true);
return;
}
// User is logged in, create the agent with files like dashboard does
setIsSubmitting(true); setIsSubmitting(true);
try { try {
const files = chatInputRef.current?.getPendingFiles() || [];
localStorage.removeItem(PENDING_PROMPT_KEY);
const formData = new FormData(); const formData = new FormData();
formData.append('prompt', inputValue.trim()); formData.append('prompt', message);
formData.append('model_name', 'openrouter/deepseek/deepseek-chat');
formData.append('enable_thinking', 'false'); // Add selected agent if one is chosen
if (selectedAgentId) {
formData.append('agent_id', selectedAgentId);
}
// Add files if any
files.forEach((file) => {
const normalizedName = normalizeFilenameToNFC(file.name);
formData.append('files', file, normalizedName);
});
if (options?.model_name) formData.append('model_name', options.model_name);
formData.append('enable_thinking', String(options?.enable_thinking ?? false));
formData.append('reasoning_effort', 'low'); formData.append('reasoning_effort', 'low');
formData.append('stream', 'true'); formData.append('stream', 'true');
formData.append('enable_context_manager', 'false'); formData.append('enable_context_manager', 'false');
const result = await initiateAgentMutation.mutateAsync(formData); const result = await initiateAgentMutation.mutateAsync(formData);
if (result.thread_id) {
setInitiatedThreadId(result.thread_id); setInitiatedThreadId(result.thread_id);
} else {
throw new Error('Agent initiation did not return a thread_id.');
}
chatInputRef.current?.clearPendingFiles();
setInputValue(''); setInputValue('');
} catch (error: any) { } catch (error: any) {
if (error instanceof BillingError) { if (error instanceof BillingError) {
console.log('Billing error:'); console.log('Billing error:', error.detail);
onOpen("paymentRequiredDialog");
} else { } else {
const isConnectionError = const isConnectionError =
error instanceof TypeError && error instanceof TypeError &&
@ -156,38 +213,6 @@ export function HeroSection() {
} }
}; };
// Handle form submission
const handleSubmit = async (e?: FormEvent) => {
if (e) {
e.preventDefault();
e.stopPropagation(); // Stop event propagation to prevent dialog closing
}
if (!inputValue.trim() || isSubmitting) return;
// If user is not logged in, save prompt and show auth dialog
if (!user && !isLoading) {
// Save prompt to localStorage BEFORE showing the dialog
localStorage.setItem(PENDING_PROMPT_KEY, inputValue.trim());
setAuthDialogOpen(true);
return;
}
// User is logged in, create the agent
createAgentWithPrompt();
};
// Handle Enter key press
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
e.preventDefault(); // Prevent default form submission
e.stopPropagation(); // Stop event propagation
handleSubmit();
}
};
return ( return (
<section id="hero" className="w-full relative overflow-hidden"> <section id="hero" className="w-full relative overflow-hidden">
<div className="relative flex flex-col items-center w-full px-6"> <div className="relative flex flex-col items-center w-full px-6">
@ -280,87 +305,27 @@ export function HeroSection() {
{hero.description} {hero.description}
</p> </p>
</div> </div>
<div className="flex items-center w-full max-w-4xl gap-2 flex-wrap justify-center"> <div className="flex items-center w-full max-w-4xl gap-2 flex-wrap justify-center">
<form className="w-full relative" onSubmit={handleSubmit}> <div className="w-full relative">
{/* Input that looks exactly like ChatInput */}
<div className="relative z-10"> <div className="relative z-10">
<Card className="shadow-none w-full max-w-8xl mx-auto bg-transparent border-none rounded-3xl overflow-hidden"> <ChatInput
<div className="w-full text-sm flex flex-col justify-between items-start rounded-2xl"> ref={chatInputRef}
<CardContent className="w-full p-1.5 pb-2 bg-card rounded-3xl border"> onSubmit={handleChatInputSubmit}
<div className="relative flex flex-col w-full h-full gap-2 justify-between">
<div className="flex flex-col gap-1 px-2">
<Textarea
value={inputValue}
onChange={(e) => setInputValue(e.target.value)}
onKeyDown={handleKeyDown}
placeholder="Describe what you need help with..." placeholder="Describe what you need help with..."
className={cn( loading={isSubmitting}
'w-full bg-transparent dark:bg-transparent border-none shadow-none focus-visible:ring-0 px-2 pb-6 pt-4 !text-[15px] min-h-[36px] max-h-[200px] overflow-y-auto resize-none'
)}
disabled={isSubmitting} disabled={isSubmitting}
rows={1} value={inputValue}
onChange={setInputValue}
isLoggedIn={!!user}
selectedAgentId={selectedAgentId}
onAgentSelect={setSelectedAgentId}
autoFocus={false}
/> />
</div> </div>
<div className="flex items-center justify-between mt-0 mb-1 px-2">
<div className="flex items-center gap-3">
{/* Attach files button */}
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
variant="outline"
size="sm"
className="h-8 px-3 py-2 bg-transparent border border-border rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2"
disabled={isSubmitting}
>
<Paperclip className="h-4 w-4" />
<span className="text-sm">Attach</span>
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Please login to attach files</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
<div className='flex items-center gap-2'>
<ChatDropdown />
<Tooltip>
<TooltipTrigger asChild>
<Button
type="submit"
size="sm"
className={cn(
'w-8 h-8 flex-shrink-0 self-end rounded-xl',
(!inputValue.trim() || isSubmitting) ? 'opacity-50' : '',
)}
disabled={!inputValue.trim() || isSubmitting}
>
{isSubmitting ? (
<Square className="h-5 w-5" />
) : (
<ArrowUp className="h-5 w-5" />
)}
</Button>
</TooltipTrigger>
<TooltipContent>
<p>Send message</p>
</TooltipContent>
</Tooltip>
</div>
</div>
</div>
</CardContent>
</div>
</Card>
</div>
{/* Subtle glow effect */} {/* Subtle glow effect */}
<div className="absolute -bottom-4 inset-x-0 h-6 bg-secondary/20 blur-xl rounded-full -z-10 opacity-70"></div> <div className="absolute -bottom-4 inset-x-0 h-6 bg-secondary/20 blur-xl rounded-full -z-10 opacity-70"></div>
</form> </div>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,8 +1,22 @@
import { HeroVideoDialog } from '@/components/home/ui/hero-video-dialog'; import { HeroVideoDialog } from '@/components/home/ui/hero-video-dialog';
import { SectionHeader } from '@/components/home/section-header';
export function HeroVideoSection() { export function HeroVideoSection() {
return ( return (
<div className="relative px-6 mt-10"> <section
id="demo"
className="flex flex-col items-center justify-center gap-10 w-full relative"
>
<SectionHeader>
<h2 className="text-3xl md:text-4xl font-medium tracking-tighter text-center text-balance pb-1">
Watch Intelligence in Motion
</h2>
<p className="text-muted-foreground text-center text-balance font-medium">
Watch how Suna executes complex workflows with precision and autonomy
</p>
</SectionHeader>
<div className="relative px-6">
<div className="relative w-full max-w-3xl mx-auto shadow-xl rounded-2xl overflow-hidden"> <div className="relative w-full max-w-3xl mx-auto shadow-xl rounded-2xl overflow-hidden">
<HeroVideoDialog <HeroVideoDialog
className="block dark:hidden" className="block dark:hidden"
@ -20,5 +34,6 @@ export function HeroVideoSection() {
/> />
</div> </div>
</div> </div>
</section>
); );
} }

View File

@ -628,7 +628,7 @@ export function PricingSection({
return ( return (
<section <section
id="pricing" id="pricing"
className={cn("flex flex-col items-center justify-center gap-10 w-full relative")} className={cn("flex flex-col items-center justify-center gap-10 w-full relative pb-12")}
> >
{showTitleAndTabs && ( {showTitleAndTabs && (
<> <>
@ -637,8 +637,7 @@ export function PricingSection({
Choose the right plan for your needs Choose the right plan for your needs
</h2> </h2>
<p className="text-muted-foreground text-center text-balance font-medium"> <p className="text-muted-foreground text-center text-balance font-medium">
Start with our free plan or upgrade to a premium plan for more Start with our free plan or upgrade for more AI token credits
usage hours
</p> </p>
</SectionHeader> </SectionHeader>
<div className="relative w-full h-full"> <div className="relative w-full h-full">
@ -691,6 +690,15 @@ export function PricingSection({
))} ))}
</div> </div>
)} )}
<div className="mt-4 p-4 bg-blue-50 dark:bg-blue-950/20 border border-blue-200 dark:border-blue-800 rounded-lg max-w-2xl mx-auto">
<p className="text-sm text-blue-800 dark:text-blue-200 text-center">
<strong>What are AI tokens?</strong> Tokens are units of text that AI models process.
Your plan includes credits to spend on various AI models - the more complex the task,
the more tokens used.
</p>
</div>
</section> </section>
); );
} }

View File

@ -7,6 +7,7 @@ import {
DropdownMenuItem, DropdownMenuItem,
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import Image from 'next/image'
const ChatDropdown = () => { const ChatDropdown = () => {
const [isOpen, setIsOpen] = useState(false) const [isOpen, setIsOpen] = useState(false)
@ -23,7 +24,7 @@ const ChatDropdown = () => {
}} }}
> >
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<User size={16} /> <Image src="/kortix-symbol.svg" alt="Suna" width={16} height={16} className="h-4 w-4 dark:invert" />
<span>Suna</span> <span>Suna</span>
<ChevronDown size={14} className="opacity-50" /> <ChevronDown size={14} className="opacity-50" />
</div> </div>

View File

@ -49,6 +49,7 @@ export interface ChatInputProps {
toolCallIndex?: number; toolCallIndex?: number;
showToolPreview?: boolean; showToolPreview?: boolean;
onExpandToolPreview?: () => void; onExpandToolPreview?: () => void;
isLoggedIn?: boolean;
} }
export interface UploadedFile { export interface UploadedFile {
@ -83,6 +84,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
toolCallIndex = 0, toolCallIndex = 0,
showToolPreview = false, showToolPreview = false,
onExpandToolPreview, onExpandToolPreview,
isLoggedIn = true,
}, },
ref, ref,
) => { ) => {
@ -303,6 +305,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
subscriptionStatus={subscriptionStatus} subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel} canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels} refreshCustomModels={refreshCustomModels}
isLoggedIn={isLoggedIn}
selectedAgentId={selectedAgentId} selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect} onAgentSelect={onAgentSelect}

View File

@ -179,6 +179,7 @@ interface FileUploadHandlerProps {
setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>; setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>;
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>; setIsUploading: React.Dispatch<React.SetStateAction<boolean>>;
messages?: any[]; // Add messages prop messages?: any[]; // Add messages prop
isLoggedIn?: boolean;
} }
export const FileUploadHandler = forwardRef< export const FileUploadHandler = forwardRef<
@ -196,6 +197,7 @@ export const FileUploadHandler = forwardRef<
setUploadedFiles, setUploadedFiles,
setIsUploading, setIsUploading,
messages = [], messages = [],
isLoggedIn = true,
}, },
ref, ref,
) => { ) => {
@ -246,6 +248,7 @@ export const FileUploadHandler = forwardRef<
<TooltipProvider> <TooltipProvider>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-block">
<Button <Button
type="button" type="button"
onClick={handleFileUpload} onClick={handleFileUpload}
@ -253,7 +256,7 @@ export const FileUploadHandler = forwardRef<
size="sm" size="sm"
className="h-8 px-3 py-2 bg-transparent border border-border rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2" className="h-8 px-3 py-2 bg-transparent border border-border rounded-xl text-muted-foreground hover:text-foreground hover:bg-accent/50 flex items-center gap-2"
disabled={ disabled={
loading || (disabled && !isAgentRunning) || isUploading !isLoggedIn || loading || (disabled && !isAgentRunning) || isUploading
} }
> >
{isUploading ? ( {isUploading ? (
@ -263,9 +266,10 @@ export const FileUploadHandler = forwardRef<
)} )}
<span className="text-sm">Attach</span> <span className="text-sm">Attach</span>
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent side="top"> <TooltipContent side="top">
<p>Attach files</p> <p>{isLoggedIn ? 'Attach files' : 'Please login to attach files'}</p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>

View File

@ -8,13 +8,14 @@ import { FileUploadHandler } from './file-upload-handler';
import { VoiceRecorder } from './voice-recorder'; import { VoiceRecorder } from './voice-recorder';
import { ModelSelector } from './model-selector'; import { ModelSelector } from './model-selector';
import { ChatSettingsDropdown } from './chat-settings-dropdown'; import { ChatSettingsDropdown } from './chat-settings-dropdown';
import { SubscriptionStatus } from './_use-model-selection'; import { canAccessModel, SubscriptionStatus } from './_use-model-selection';
import { isLocalMode } from '@/lib/config'; import { isLocalMode } from '@/lib/config';
import { useFeatureFlag } from '@/lib/feature-flags'; import { useFeatureFlag } from '@/lib/feature-flags';
import { TooltipContent } from '@/components/ui/tooltip'; import { TooltipContent } from '@/components/ui/tooltip';
import { Tooltip } from '@/components/ui/tooltip'; import { Tooltip } from '@/components/ui/tooltip';
import { TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip'; import { TooltipProvider, TooltipTrigger } from '@radix-ui/react-tooltip';
import { BillingModal } from '@/components/billing/billing-modal'; import { BillingModal } from '@/components/billing/billing-modal';
import ChatDropdown from './chat-dropdown';
interface MessageInputProps { interface MessageInputProps {
value: string; value: string;
@ -37,6 +38,7 @@ interface MessageInputProps {
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>; setIsUploading: React.Dispatch<React.SetStateAction<boolean>>;
hideAttachments?: boolean; hideAttachments?: boolean;
messages?: any[]; // Add messages prop messages?: any[]; // Add messages prop
isLoggedIn?: boolean;
selectedModel: string; selectedModel: string;
onModelChange: (model: string) => void; onModelChange: (model: string) => void;
@ -71,6 +73,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
setIsUploading, setIsUploading,
hideAttachments = false, hideAttachments = false,
messages = [], messages = [],
isLoggedIn = true,
selectedModel, selectedModel,
onModelChange, onModelChange,
@ -122,6 +125,36 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
} }
}; };
const renderDropdown = () => {
if (isLoggedIn) {
if (!customAgentsEnabled || flagsLoading) {
return <ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
/>
} else {
return <ChatSettingsDropdown
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
disabled={loading || (disabled && !isAgentRunning)}
/>
}
}
return <ChatDropdown />;
}
return ( return (
<div className="relative flex flex-col w-full h-full gap-2 justify-between"> <div className="relative flex flex-col w-full h-full gap-2 justify-between">
@ -156,6 +189,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
setUploadedFiles={setUploadedFiles} setUploadedFiles={setUploadedFiles}
setIsUploading={setIsUploading} setIsUploading={setIsUploading}
messages={messages} messages={messages}
isLoggedIn={isLoggedIn}
/> />
)} )}
@ -176,30 +210,7 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
<div className='flex items-center gap-2'> <div className='flex items-center gap-2'>
{/* Show model selector inline if custom agents are disabled, otherwise show settings dropdown */} {/* Show model selector inline if custom agents are disabled, otherwise show settings dropdown */}
{!customAgentsEnabled || flagsLoading ? ( {renderDropdown()}
<ModelSelector
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
billingModalOpen={billingModalOpen}
setBillingModalOpen={setBillingModalOpen}
/>
) : (
<ChatSettingsDropdown
selectedAgentId={selectedAgentId}
onAgentSelect={onAgentSelect}
selectedModel={selectedModel}
onModelChange={onModelChange}
modelOptions={modelOptions}
subscriptionStatus={subscriptionStatus}
canAccessModel={canAccessModel}
refreshCustomModels={refreshCustomModels}
disabled={loading || (disabled && !isAgentRunning)}
/>
)}
{/* Billing Modal */} {/* Billing Modal */}
<BillingModal <BillingModal
@ -208,10 +219,10 @@ export const MessageInput = forwardRef<HTMLTextAreaElement, MessageInputProps>(
returnUrl={typeof window !== 'undefined' ? window.location.href : '/'} returnUrl={typeof window !== 'undefined' ? window.location.href : '/'}
/> />
<VoiceRecorder {isLoggedIn && <VoiceRecorder
onTranscription={onTranscription} onTranscription={onTranscription}
disabled={loading || (disabled && !isAgentRunning)} disabled={loading || (disabled && !isAgentRunning)}
/> />}
<Button <Button
type="submit" type="submit"

View File

@ -115,16 +115,17 @@ export const siteConfig = {
{ {
name: 'Free', name: 'Free',
price: '$0', price: '$0',
description: 'Get started with', description: 'Perfect for getting started',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white', buttonColor: 'bg-secondary text-white',
isPopular: false, isPopular: false,
/** @deprecated */ /** @deprecated */
hours: '60 min', hours: '60 min',
features: [ features: [
'Free $5/month usage included', '$5 free AI tokens included',
'Public Projects', 'Public projects',
'Limited models', 'Basic Models',
'Community support',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.FREE.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.FREE.priceId,
upgradePlans: [], upgradePlans: [],
@ -135,17 +136,17 @@ export const siteConfig = {
yearlyPrice: '$204', yearlyPrice: '$204',
originalYearlyPrice: '$240', originalYearlyPrice: '$240',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Free, plus:', description: 'Best for individuals and small teams',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-primary text-white dark:text-black', buttonColor: 'bg-primary text-white dark:text-black',
isPopular: true, isPopular: true,
/** @deprecated */ /** @deprecated */
hours: '2 hours', hours: '2 hours',
features: [ features: [
'$20/month usage', '$20 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'More models', 'Premium AI Models',
'Community support',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_2_20_YEARLY.priceId,
@ -157,17 +158,17 @@ export const siteConfig = {
yearlyPrice: '$510', yearlyPrice: '$510',
originalYearlyPrice: '$600', originalYearlyPrice: '$600',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Free, plus:', description: 'Ideal for growing businesses',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white', buttonColor: 'bg-secondary text-white',
isPopular: false, isPopular: false,
/** @deprecated */ /** @deprecated */
hours: '6 hours', hours: '6 hours',
features: [ features: [
'$50/month usage', '$50 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'More models', 'Premium AI Models',
'Community support',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_6_50_YEARLY.priceId,
@ -179,16 +180,16 @@ export const siteConfig = {
yearlyPrice: '$1020', yearlyPrice: '$1020',
originalYearlyPrice: '$1200', originalYearlyPrice: '$1200',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Pro, plus:', description: 'For established businesses',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white', buttonColor: 'bg-secondary text-white',
isPopular: false, isPopular: false,
hours: '12 hours', hours: '12 hours',
features: [ features: [
'$100/month usage', '$100 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'Priority support', 'Premium AI Models',
'Community support',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_12_100_YEARLY.priceId,
@ -201,16 +202,16 @@ export const siteConfig = {
yearlyPrice: '$2040', yearlyPrice: '$2040',
originalYearlyPrice: '$2400', originalYearlyPrice: '$2400',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Free, plus:', description: 'For power users and teams',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-primary text-white dark:text-black', buttonColor: 'bg-primary text-white dark:text-black',
isPopular: false, isPopular: false,
hours: '25 hours', hours: '25 hours',
features: [ features: [
'$200/month usage', '$200 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'More models', 'Premium AI Models',
'Priority support',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_25_200_YEARLY.priceId,
@ -222,18 +223,19 @@ export const siteConfig = {
yearlyPrice: '$4080', yearlyPrice: '$4080',
originalYearlyPrice: '$4800', originalYearlyPrice: '$4800',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Ultra, plus:', description: 'For large organizations',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white', buttonColor: 'bg-secondary text-white',
isPopular: false, isPopular: false,
hours: '50 hours', hours: '50 hours',
features: [ features: [
'$400/month usage', '$400 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'Access to intelligent Model (Full Suna)', 'Premium AI Models',
'Priority support', 'Full Suna AI access',
'Community support',
'Custom integrations', 'Custom integrations',
'Dedicated account manager',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_50_400_YEARLY.priceId,
@ -246,19 +248,20 @@ export const siteConfig = {
yearlyPrice: '$8160', yearlyPrice: '$8160',
originalYearlyPrice: '$9600', originalYearlyPrice: '$9600',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Enterprise, plus:', description: 'For scaling enterprises',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white', buttonColor: 'bg-secondary text-white',
isPopular: false, isPopular: false,
hours: '125 hours', hours: '125 hours',
features: [ features: [
'$800/month usage', '$800 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'Access to intelligent Model (Full Suna)', 'Premium AI Models',
'Priority support', 'Full Suna AI access',
'Community support',
'Custom integrations', 'Custom integrations',
'Dedicated account manager', 'Dedicated account manager',
'Custom SLA',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_125_800_YEARLY.priceId,
@ -271,20 +274,21 @@ export const siteConfig = {
yearlyPrice: '$10200', yearlyPrice: '$10200',
originalYearlyPrice: '$12000', originalYearlyPrice: '$12000',
discountPercentage: 15, discountPercentage: 15,
description: 'Everything in Scale, plus:', description: 'For maximum scale and performance',
buttonText: 'Start Free', buttonText: 'Start Free',
buttonColor: 'bg-secondary text-white', buttonColor: 'bg-secondary text-white',
isPopular: false, isPopular: false,
hours: '200 hours', hours: '200 hours',
features: [ features: [
'$1000/month usage', '$1000 AI token credits/month',
// '+ $5 free included',
'Private projects', 'Private projects',
'Access to intelligent Model (Full Suna)', 'Premium AI Models',
'Full Suna AI access',
'Priority support', 'Priority support',
'Custom integrations', 'Custom integrations',
'Dedicated account manager', 'Dedicated account manager',
'Custom SLA', 'Custom SLA',
'White-label options',
], ],
stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId, stripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000.priceId,
yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000_YEARLY.priceId, yearlyStripePriceId: config.SUBSCRIPTION_TIERS.TIER_200_1000_YEARLY.priceId,