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)
return agent_run_data
@router.post("/thread/{thread_id}/agent/start")
async def start_agent(
thread_id: str,

View File

@ -10,30 +10,6 @@ class SandboxExposeTool(SandboxToolsBase):
def __init__(self, project_id: str, thread_manager: ThreadManager):
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({
"type": "function",
"function": {
@ -93,11 +69,6 @@ class SandboxExposeTool(SandboxToolsBase):
if not 1 <= port <= 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)
if port not in [6080, 8080, 8003]: # Skip check for known sandbox ports
try:

View File

@ -28,7 +28,7 @@ export default function Home() {
<div className='flex flex-col items-center px-4'>
<PricingSection />
</div>
<div className="mt-12 pb-12 mx-auto">
<div className="pb-10 mx-auto">
<HeroVideoSection />
</div>
{/* <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 { generateThreadName } from '@/lib/actions/threads';
import GoogleSignIn from '@/components/GoogleSignIn';
import { useAgents } from '@/hooks/react-query/agents/use-agents';
import {
Dialog,
DialogContent,
@ -30,13 +31,11 @@ import { useAccounts } from '@/hooks/use-accounts';
import { isLocalMode, config } from '@/lib/config';
import { toast } from 'sonner';
import { useModal } from '@/hooks/use-modal-store';
import { Card, CardContent } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Send, ArrowUp, Paperclip } from 'lucide-react';
import { Textarea } from '@/components/ui/textarea';
import { cn } from '@/lib/utils';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
import ChatDropdown from '@/components/thread/chat-input/chat-dropdown';
import { ChatInput, ChatInputHandles } from '@/components/thread/chat-input/chat-input';
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
import { createQueryHook } from '@/hooks/use-query';
import { agentKeys } from '@/hooks/react-query/agents/keys';
import { getAgents } from '@/hooks/react-query/agents/utils';
// Custom dialog overlay with blur effect
const BlurredDialogOverlay = () => (
@ -55,6 +54,7 @@ export function HeroSection() {
const scrollTimeout = useRef<NodeJS.Timeout | null>(null);
const { scrollY } = useScroll();
const [inputValue, setInputValue] = useState('');
const [selectedAgentId, setSelectedAgentId] = useState<string | undefined>();
const router = useRouter();
const { user, isLoading } = useAuth();
const { billingError, handleBillingError, clearBillingError } =
@ -65,6 +65,28 @@ export function HeroSection() {
const initiateAgentMutation = useInitiateAgentMutation();
const [initiatedThreadId, setInitiatedThreadId] = useState<string | null>(null);
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
const [authDialogOpen, setAuthDialogOpen] = useState(false);
@ -116,31 +138,66 @@ export function HeroSection() {
if (thread.project_id) {
router.push(`/projects/${thread.project_id}/thread/${initiatedThreadId}`);
} else {
router.push(`/thread/${initiatedThreadId}`);
router.push(`/agents/${initiatedThreadId}`);
}
setInitiatedThreadId(null);
}
}, [threadQuery.data, initiatedThreadId, router]);
const createAgentWithPrompt = async () => {
if (!inputValue.trim() || isSubmitting) return;
// Handle ChatInput submission
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);
try {
const files = chatInputRef.current?.getPendingFiles() || [];
localStorage.removeItem(PENDING_PROMPT_KEY);
const formData = new FormData();
formData.append('prompt', inputValue.trim());
formData.append('model_name', 'openrouter/deepseek/deepseek-chat');
formData.append('enable_thinking', 'false');
formData.append('prompt', message);
// 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('stream', 'true');
formData.append('enable_context_manager', 'false');
const result = await initiateAgentMutation.mutateAsync(formData);
setInitiatedThreadId(result.thread_id);
if (result.thread_id) {
setInitiatedThreadId(result.thread_id);
} else {
throw new Error('Agent initiation did not return a thread_id.');
}
chatInputRef.current?.clearPendingFiles();
setInputValue('');
} catch (error: any) {
if (error instanceof BillingError) {
console.log('Billing error:');
console.log('Billing error:', error.detail);
onOpen("paymentRequiredDialog");
} else {
const isConnectionError =
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 (
<section id="hero" className="w-full relative overflow-hidden">
<div className="relative flex flex-col items-center w-full px-6">
@ -280,87 +305,27 @@ export function HeroSection() {
{hero.description}
</p>
</div>
<div className="flex items-center w-full max-w-4xl gap-2 flex-wrap justify-center">
<form className="w-full relative" onSubmit={handleSubmit}>
{/* Input that looks exactly like ChatInput */}
<div className="w-full relative">
<div className="relative z-10">
<Card className="shadow-none w-full max-w-8xl mx-auto bg-transparent border-none rounded-3xl overflow-hidden">
<div className="w-full text-sm flex flex-col justify-between items-start rounded-2xl">
<CardContent className="w-full p-1.5 pb-2 bg-card rounded-3xl border">
<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..."
className={cn(
'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}
rows={1}
/>
</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>
<ChatInput
ref={chatInputRef}
onSubmit={handleChatInputSubmit}
placeholder="Describe what you need help with..."
loading={isSubmitting}
disabled={isSubmitting}
value={inputValue}
onChange={setInputValue}
isLoggedIn={!!user}
selectedAgentId={selectedAgentId}
onAgentSelect={setSelectedAgentId}
autoFocus={false}
/>
</div>
{/* 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>
</form>
</div>
</div>
</div>
</div>

View File

@ -1,24 +1,39 @@
import { HeroVideoDialog } from '@/components/home/ui/hero-video-dialog';
import { SectionHeader } from '@/components/home/section-header';
export function HeroVideoSection() {
return (
<div className="relative px-6 mt-10">
<div className="relative w-full max-w-3xl mx-auto shadow-xl rounded-2xl overflow-hidden">
<HeroVideoDialog
className="block dark:hidden"
animationStyle="from-center"
videoSrc="https://www.youtube.com/embed/Jnxq0osSg2c?si=k8ddEM8h8lver20s"
thumbnailSrc="/thumbnail-light.png"
thumbnailAlt="Hero Video"
/>
<HeroVideoDialog
className="hidden dark:block"
animationStyle="from-center"
videoSrc="https://www.youtube.com/embed/Jnxq0osSg2c?si=k8ddEM8h8lver20s"
thumbnailSrc="/thumbnail-dark.png"
thumbnailAlt="Hero Video"
/>
<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">
<HeroVideoDialog
className="block dark:hidden"
animationStyle="from-center"
videoSrc="https://www.youtube.com/embed/Jnxq0osSg2c?si=k8ddEM8h8lver20s"
thumbnailSrc="/thumbnail-light.png"
thumbnailAlt="Hero Video"
/>
<HeroVideoDialog
className="hidden dark:block"
animationStyle="from-center"
videoSrc="https://www.youtube.com/embed/Jnxq0osSg2c?si=k8ddEM8h8lver20s"
thumbnailSrc="/thumbnail-dark.png"
thumbnailAlt="Hero Video"
/>
</div>
</div>
</div>
</section>
);
}

View File

@ -124,30 +124,30 @@ function PriceDisplay({ price, isCompact }: PriceDisplayProps) {
);
}
function BillingPeriodToggle({
billingPeriod,
setBillingPeriod
}: {
function BillingPeriodToggle({
billingPeriod,
setBillingPeriod
}: {
billingPeriod: 'monthly' | 'yearly';
setBillingPeriod: (period: 'monthly' | 'yearly') => void;
}) {
return (
<div className="flex items-center justify-center gap-3">
<div
<div
className="relative bg-muted rounded-full p-1 cursor-pointer"
onClick={() => setBillingPeriod(billingPeriod === 'monthly' ? 'yearly' : 'monthly')}
>
<div className="flex">
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200",
billingPeriod === 'monthly'
? 'bg-background text-foreground shadow-sm'
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200",
billingPeriod === 'monthly'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground'
)}>
Monthly
</div>
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200 flex items-center gap-1",
billingPeriod === 'yearly'
? 'bg-background text-foreground shadow-sm'
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200 flex items-center gap-1",
billingPeriod === 'yearly'
? 'bg-background text-foreground shadow-sm'
: 'text-muted-foreground'
)}>
Yearly
@ -256,18 +256,18 @@ function PricingTier({
}
};
const tierPriceId = billingPeriod === 'yearly' && tier.yearlyStripePriceId
? tier.yearlyStripePriceId
const tierPriceId = billingPeriod === 'yearly' && tier.yearlyStripePriceId
? tier.yearlyStripePriceId
: tier.stripePriceId;
const displayPrice = billingPeriod === 'yearly' && tier.yearlyPrice
? tier.yearlyPrice
const displayPrice = billingPeriod === 'yearly' && tier.yearlyPrice
? tier.yearlyPrice
: tier.price;
// Find the current tier (moved outside conditional for JSX access)
const currentTier = siteConfig.cloudPricingItems.find(
(p) => p.stripePriceId === currentSubscription?.price_id || p.yearlyStripePriceId === currentSubscription?.price_id,
);
const isCurrentActivePlan =
isAuthenticated && currentSubscription?.price_id === tierPriceId;
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
@ -337,7 +337,7 @@ function PricingTier({
const currentIsYearly = currentTier && currentSubscription?.price_id === currentTier.yearlyStripePriceId;
const targetIsMonthly = tier.stripePriceId === tierPriceId;
const targetIsYearly = tier.yearlyStripePriceId === tierPriceId;
const isSameTierDifferentBilling = currentTier && currentTier.name === tier.name &&
const isSameTierDifferentBilling = currentTier && currentTier.name === tier.name &&
((currentIsMonthly && targetIsYearly) || (currentIsYearly && targetIsMonthly));
if (
@ -419,8 +419,8 @@ function PricingTier({
<div
className={cn(
'rounded-xl flex flex-col relative',
insideDialog
? 'min-h-[300px]'
insideDialog
? 'min-h-[300px]'
: 'h-full min-h-[300px]',
tier.isPopular && !insideDialog
? 'md:shadow-[0px_61px_24px_-10px_rgba(0,0,0,0.01),0px_34px_20px_-8px_rgba(0,0,0,0.05),0px_15px_15px_-6px_rgba(0,0,0,0.09),0px_4px_8px_-2px_rgba(0,0,0,0.10),0px_0px_0px_1px_rgba(0,0,0,0.08)] bg-accent'
@ -440,14 +440,14 @@ function PricingTier({
</span>
)}
{/* Show upgrade badge for yearly plans when user is on monthly */}
{!tier.isPopular && isAuthenticated && currentSubscription && billingPeriod === 'yearly' &&
currentTier && currentSubscription.price_id === currentTier.stripePriceId &&
tier.yearlyStripePriceId && (currentTier.name === tier.name ||
parseFloat(tier.price.slice(1)) >= parseFloat(currentTier.price.slice(1))) && (
<span className="bg-green-500/10 text-green-700 text-[10px] font-medium px-1.5 py-0.5 rounded-full">
Recommended
</span>
)}
{!tier.isPopular && isAuthenticated && currentSubscription && billingPeriod === 'yearly' &&
currentTier && currentSubscription.price_id === currentTier.stripePriceId &&
tier.yearlyStripePriceId && (currentTier.name === tier.name ||
parseFloat(tier.price.slice(1)) >= parseFloat(currentTier.price.slice(1))) && (
<span className="bg-green-500/10 text-green-700 text-[10px] font-medium px-1.5 py-0.5 rounded-full">
Recommended
</span>
)}
{isAuthenticated && statusBadge}
</p>
<div className="flex items-baseline mt-2">
@ -481,7 +481,7 @@ function PricingTier({
</div>
) : (
<div className="hidden items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold bg-primary/10 border-primary/20 text-primary w-fit">
{billingPeriod === 'yearly' && tier.yearlyPrice && displayPrice !== '$0'
{billingPeriod === 'yearly' && tier.yearlyPrice && displayPrice !== '$0'
? `$${Math.round(parseFloat(tier.yearlyPrice.slice(1)) / 12)}/month (billed yearly)`
: `${displayPrice}/month`
}
@ -628,7 +628,7 @@ export function PricingSection({
return (
<section
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 && (
<>
@ -637,8 +637,7 @@ export function PricingSection({
Choose the right plan for your needs
</h2>
<p className="text-muted-foreground text-center text-balance font-medium">
Start with our free plan or upgrade to a premium plan for more
usage hours
Start with our free plan or upgrade for more AI token credits
</p>
</SectionHeader>
<div className="relative w-full h-full">
@ -654,9 +653,9 @@ export function PricingSection({
)}
{deploymentType === 'cloud' && (
<BillingPeriodToggle
billingPeriod={billingPeriod}
setBillingPeriod={setBillingPeriod}
<BillingPeriodToggle
billingPeriod={billingPeriod}
setBillingPeriod={setBillingPeriod}
/>
)}
@ -667,7 +666,7 @@ export function PricingSection({
"px-6 max-w-7xl": !insideDialog,
"max-w-7xl": insideDialog
},
insideDialog
insideDialog
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 2xl:grid-cols-4"
: "min-[650px]:grid-cols-2 lg:grid-cols-4",
!insideDialog && "grid-rows-1 items-stretch"
@ -675,22 +674,31 @@ export function PricingSection({
{siteConfig.cloudPricingItems
.filter((tier) => !tier.hidden && (!hideFree || tier.price !== '$0'))
.map((tier) => (
<PricingTier
key={tier.name}
tier={tier}
currentSubscription={currentSubscription}
isLoading={planLoadingStates}
isFetchingPlan={isFetchingPlan}
onPlanSelect={handlePlanSelect}
onSubscriptionUpdate={handleSubscriptionUpdate}
isAuthenticated={isAuthenticated}
returnUrl={returnUrl}
insideDialog={insideDialog}
billingPeriod={billingPeriod}
/>
))}
<PricingTier
key={tier.name}
tier={tier}
currentSubscription={currentSubscription}
isLoading={planLoadingStates}
isFetchingPlan={isFetchingPlan}
onPlanSelect={handlePlanSelect}
onSubscriptionUpdate={handleSubscriptionUpdate}
isAuthenticated={isAuthenticated}
returnUrl={returnUrl}
insideDialog={insideDialog}
billingPeriod={billingPeriod}
/>
))}
</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>
);
}

View File

@ -7,6 +7,7 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import Image from 'next/image'
const ChatDropdown = () => {
const [isOpen, setIsOpen] = useState(false)
@ -23,7 +24,7 @@ const ChatDropdown = () => {
}}
>
<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>
<ChevronDown size={14} className="opacity-50" />
</div>

View File

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

View File

@ -179,6 +179,7 @@ interface FileUploadHandlerProps {
setUploadedFiles: React.Dispatch<React.SetStateAction<UploadedFile[]>>;
setIsUploading: React.Dispatch<React.SetStateAction<boolean>>;
messages?: any[]; // Add messages prop
isLoggedIn?: boolean;
}
export const FileUploadHandler = forwardRef<
@ -196,6 +197,7 @@ export const FileUploadHandler = forwardRef<
setUploadedFiles,
setIsUploading,
messages = [],
isLoggedIn = true,
},
ref,
) => {
@ -246,26 +248,28 @@ export const FileUploadHandler = forwardRef<
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
type="button"
onClick={handleFileUpload}
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={
loading || (disabled && !isAgentRunning) || isUploading
}
>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Paperclip className="h-4 w-4" />
)}
<span className="text-sm">Attach</span>
</Button>
<span className="inline-block">
<Button
type="button"
onClick={handleFileUpload}
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={
!isLoggedIn || loading || (disabled && !isAgentRunning) || isUploading
}
>
{isUploading ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Paperclip className="h-4 w-4" />
)}
<span className="text-sm">Attach</span>
</Button>
</span>
</TooltipTrigger>
<TooltipContent side="top">
<p>Attach files</p>
<p>{isLoggedIn ? 'Attach files' : 'Please login to attach files'}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

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

View File

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