mirror of https://github.com/kortix-ai/suna.git
Merge branch 'main' of github.com:escapade-mckv/suna
This commit is contained in:
commit
18eb26a423
|
@ -255,6 +255,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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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 /> */}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Reference in New Issue