mirror of https://github.com/kortix-ai/suna.git
Merge branch 'main' into pipedream-mcp
This commit is contained in:
commit
c119b1dc5d
|
@ -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,
|
||||||
|
|
|
@ -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:
|
||||||
|
|
|
@ -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 /> */}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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}
|
||||||
|
|
|
@ -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>
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Reference in New Issue