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);
|
||||||
|
|
||||||
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('');
|
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">
|
placeholder="Describe what you need help with..."
|
||||||
<div className="flex flex-col gap-1 px-2">
|
loading={isSubmitting}
|
||||||
<Textarea
|
disabled={isSubmitting}
|
||||||
value={inputValue}
|
value={inputValue}
|
||||||
onChange={(e) => setInputValue(e.target.value)}
|
onChange={setInputValue}
|
||||||
onKeyDown={handleKeyDown}
|
isLoggedIn={!!user}
|
||||||
placeholder="Describe what you need help with..."
|
selectedAgentId={selectedAgentId}
|
||||||
className={cn(
|
onAgentSelect={setSelectedAgentId}
|
||||||
'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'
|
autoFocus={false}
|
||||||
)}
|
/>
|
||||||
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>
|
|
||||||
</div>
|
</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,24 +1,39 @@
|
||||||
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
|
||||||
<div className="relative w-full max-w-3xl mx-auto shadow-xl rounded-2xl overflow-hidden">
|
id="demo"
|
||||||
<HeroVideoDialog
|
className="flex flex-col items-center justify-center gap-10 w-full relative"
|
||||||
className="block dark:hidden"
|
>
|
||||||
animationStyle="from-center"
|
<SectionHeader>
|
||||||
videoSrc="https://www.youtube.com/embed/Jnxq0osSg2c?si=k8ddEM8h8lver20s"
|
<h2 className="text-3xl md:text-4xl font-medium tracking-tighter text-center text-balance pb-1">
|
||||||
thumbnailSrc="/thumbnail-light.png"
|
Watch Intelligence in Motion
|
||||||
thumbnailAlt="Hero Video"
|
</h2>
|
||||||
/>
|
<p className="text-muted-foreground text-center text-balance font-medium">
|
||||||
<HeroVideoDialog
|
Watch how Suna executes complex workflows with precision and autonomy
|
||||||
className="hidden dark:block"
|
</p>
|
||||||
animationStyle="from-center"
|
</SectionHeader>
|
||||||
videoSrc="https://www.youtube.com/embed/Jnxq0osSg2c?si=k8ddEM8h8lver20s"
|
|
||||||
thumbnailSrc="/thumbnail-dark.png"
|
<div className="relative px-6">
|
||||||
thumbnailAlt="Hero Video"
|
<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>
|
||||||
</div>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -124,30 +124,30 @@ function PriceDisplay({ price, isCompact }: PriceDisplayProps) {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function BillingPeriodToggle({
|
function BillingPeriodToggle({
|
||||||
billingPeriod,
|
billingPeriod,
|
||||||
setBillingPeriod
|
setBillingPeriod
|
||||||
}: {
|
}: {
|
||||||
billingPeriod: 'monthly' | 'yearly';
|
billingPeriod: 'monthly' | 'yearly';
|
||||||
setBillingPeriod: (period: 'monthly' | 'yearly') => void;
|
setBillingPeriod: (period: 'monthly' | 'yearly') => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center gap-3">
|
<div className="flex items-center justify-center gap-3">
|
||||||
<div
|
<div
|
||||||
className="relative bg-muted rounded-full p-1 cursor-pointer"
|
className="relative bg-muted rounded-full p-1 cursor-pointer"
|
||||||
onClick={() => setBillingPeriod(billingPeriod === 'monthly' ? 'yearly' : 'monthly')}
|
onClick={() => setBillingPeriod(billingPeriod === 'monthly' ? 'yearly' : 'monthly')}
|
||||||
>
|
>
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200",
|
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200",
|
||||||
billingPeriod === 'monthly'
|
billingPeriod === 'monthly'
|
||||||
? 'bg-background text-foreground shadow-sm'
|
? 'bg-background text-foreground shadow-sm'
|
||||||
: 'text-muted-foreground'
|
: 'text-muted-foreground'
|
||||||
)}>
|
)}>
|
||||||
Monthly
|
Monthly
|
||||||
</div>
|
</div>
|
||||||
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200 flex items-center gap-1",
|
<div className={cn("px-3 py-1 rounded-full text-xs font-medium transition-all duration-200 flex items-center gap-1",
|
||||||
billingPeriod === 'yearly'
|
billingPeriod === 'yearly'
|
||||||
? 'bg-background text-foreground shadow-sm'
|
? 'bg-background text-foreground shadow-sm'
|
||||||
: 'text-muted-foreground'
|
: 'text-muted-foreground'
|
||||||
)}>
|
)}>
|
||||||
Yearly
|
Yearly
|
||||||
|
@ -256,18 +256,18 @@ function PricingTier({
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const tierPriceId = billingPeriod === 'yearly' && tier.yearlyStripePriceId
|
const tierPriceId = billingPeriod === 'yearly' && tier.yearlyStripePriceId
|
||||||
? tier.yearlyStripePriceId
|
? tier.yearlyStripePriceId
|
||||||
: tier.stripePriceId;
|
: tier.stripePriceId;
|
||||||
const displayPrice = billingPeriod === 'yearly' && tier.yearlyPrice
|
const displayPrice = billingPeriod === 'yearly' && tier.yearlyPrice
|
||||||
? tier.yearlyPrice
|
? tier.yearlyPrice
|
||||||
: tier.price;
|
: tier.price;
|
||||||
|
|
||||||
// Find the current tier (moved outside conditional for JSX access)
|
// Find the current tier (moved outside conditional for JSX access)
|
||||||
const currentTier = siteConfig.cloudPricingItems.find(
|
const currentTier = siteConfig.cloudPricingItems.find(
|
||||||
(p) => p.stripePriceId === currentSubscription?.price_id || p.yearlyStripePriceId === currentSubscription?.price_id,
|
(p) => p.stripePriceId === currentSubscription?.price_id || p.yearlyStripePriceId === currentSubscription?.price_id,
|
||||||
);
|
);
|
||||||
|
|
||||||
const isCurrentActivePlan =
|
const isCurrentActivePlan =
|
||||||
isAuthenticated && currentSubscription?.price_id === tierPriceId;
|
isAuthenticated && currentSubscription?.price_id === tierPriceId;
|
||||||
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
|
const isScheduled = isAuthenticated && currentSubscription?.has_schedule;
|
||||||
|
@ -337,7 +337,7 @@ function PricingTier({
|
||||||
const currentIsYearly = currentTier && currentSubscription?.price_id === currentTier.yearlyStripePriceId;
|
const currentIsYearly = currentTier && currentSubscription?.price_id === currentTier.yearlyStripePriceId;
|
||||||
const targetIsMonthly = tier.stripePriceId === tierPriceId;
|
const targetIsMonthly = tier.stripePriceId === tierPriceId;
|
||||||
const targetIsYearly = tier.yearlyStripePriceId === tierPriceId;
|
const targetIsYearly = tier.yearlyStripePriceId === tierPriceId;
|
||||||
const isSameTierDifferentBilling = currentTier && currentTier.name === tier.name &&
|
const isSameTierDifferentBilling = currentTier && currentTier.name === tier.name &&
|
||||||
((currentIsMonthly && targetIsYearly) || (currentIsYearly && targetIsMonthly));
|
((currentIsMonthly && targetIsYearly) || (currentIsYearly && targetIsMonthly));
|
||||||
|
|
||||||
if (
|
if (
|
||||||
|
@ -419,8 +419,8 @@ function PricingTier({
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={cn(
|
||||||
'rounded-xl flex flex-col relative',
|
'rounded-xl flex flex-col relative',
|
||||||
insideDialog
|
insideDialog
|
||||||
? 'min-h-[300px]'
|
? 'min-h-[300px]'
|
||||||
: 'h-full min-h-[300px]',
|
: 'h-full min-h-[300px]',
|
||||||
tier.isPopular && !insideDialog
|
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'
|
? '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>
|
</span>
|
||||||
)}
|
)}
|
||||||
{/* Show upgrade badge for yearly plans when user is on monthly */}
|
{/* Show upgrade badge for yearly plans when user is on monthly */}
|
||||||
{!tier.isPopular && isAuthenticated && currentSubscription && billingPeriod === 'yearly' &&
|
{!tier.isPopular && isAuthenticated && currentSubscription && billingPeriod === 'yearly' &&
|
||||||
currentTier && currentSubscription.price_id === currentTier.stripePriceId &&
|
currentTier && currentSubscription.price_id === currentTier.stripePriceId &&
|
||||||
tier.yearlyStripePriceId && (currentTier.name === tier.name ||
|
tier.yearlyStripePriceId && (currentTier.name === tier.name ||
|
||||||
parseFloat(tier.price.slice(1)) >= parseFloat(currentTier.price.slice(1))) && (
|
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">
|
<span className="bg-green-500/10 text-green-700 text-[10px] font-medium px-1.5 py-0.5 rounded-full">
|
||||||
Recommended
|
Recommended
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
{isAuthenticated && statusBadge}
|
{isAuthenticated && statusBadge}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex items-baseline mt-2">
|
<div className="flex items-baseline mt-2">
|
||||||
|
@ -481,7 +481,7 @@ function PricingTier({
|
||||||
</div>
|
</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">
|
<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)`
|
? `$${Math.round(parseFloat(tier.yearlyPrice.slice(1)) / 12)}/month (billed yearly)`
|
||||||
: `${displayPrice}/month`
|
: `${displayPrice}/month`
|
||||||
}
|
}
|
||||||
|
@ -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">
|
||||||
|
@ -654,9 +653,9 @@ export function PricingSection({
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{deploymentType === 'cloud' && (
|
{deploymentType === 'cloud' && (
|
||||||
<BillingPeriodToggle
|
<BillingPeriodToggle
|
||||||
billingPeriod={billingPeriod}
|
billingPeriod={billingPeriod}
|
||||||
setBillingPeriod={setBillingPeriod}
|
setBillingPeriod={setBillingPeriod}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
@ -667,7 +666,7 @@ export function PricingSection({
|
||||||
"px-6 max-w-7xl": !insideDialog,
|
"px-6 max-w-7xl": !insideDialog,
|
||||||
"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"
|
? "grid-cols-1 sm:grid-cols-2 lg:grid-cols-2 2xl:grid-cols-4"
|
||||||
: "min-[650px]:grid-cols-2 lg:grid-cols-4",
|
: "min-[650px]:grid-cols-2 lg:grid-cols-4",
|
||||||
!insideDialog && "grid-rows-1 items-stretch"
|
!insideDialog && "grid-rows-1 items-stretch"
|
||||||
|
@ -675,22 +674,31 @@ export function PricingSection({
|
||||||
{siteConfig.cloudPricingItems
|
{siteConfig.cloudPricingItems
|
||||||
.filter((tier) => !tier.hidden && (!hideFree || tier.price !== '$0'))
|
.filter((tier) => !tier.hidden && (!hideFree || tier.price !== '$0'))
|
||||||
.map((tier) => (
|
.map((tier) => (
|
||||||
<PricingTier
|
<PricingTier
|
||||||
key={tier.name}
|
key={tier.name}
|
||||||
tier={tier}
|
tier={tier}
|
||||||
currentSubscription={currentSubscription}
|
currentSubscription={currentSubscription}
|
||||||
isLoading={planLoadingStates}
|
isLoading={planLoadingStates}
|
||||||
isFetchingPlan={isFetchingPlan}
|
isFetchingPlan={isFetchingPlan}
|
||||||
onPlanSelect={handlePlanSelect}
|
onPlanSelect={handlePlanSelect}
|
||||||
onSubscriptionUpdate={handleSubscriptionUpdate}
|
onSubscriptionUpdate={handleSubscriptionUpdate}
|
||||||
isAuthenticated={isAuthenticated}
|
isAuthenticated={isAuthenticated}
|
||||||
returnUrl={returnUrl}
|
returnUrl={returnUrl}
|
||||||
insideDialog={insideDialog}
|
insideDialog={insideDialog}
|
||||||
billingPeriod={billingPeriod}
|
billingPeriod={billingPeriod}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
</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,26 +248,28 @@ export const FileUploadHandler = forwardRef<
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Button
|
<span className="inline-block">
|
||||||
type="button"
|
<Button
|
||||||
onClick={handleFileUpload}
|
type="button"
|
||||||
variant="outline"
|
onClick={handleFileUpload}
|
||||||
size="sm"
|
variant="outline"
|
||||||
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"
|
size="sm"
|
||||||
disabled={
|
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"
|
||||||
loading || (disabled && !isAgentRunning) || isUploading
|
disabled={
|
||||||
}
|
!isLoggedIn || loading || (disabled && !isAgentRunning) || isUploading
|
||||||
>
|
}
|
||||||
{isUploading ? (
|
>
|
||||||
<Loader2 className="h-4 w-4 animate-spin" />
|
{isUploading ? (
|
||||||
) : (
|
<Loader2 className="h-4 w-4 animate-spin" />
|
||||||
<Paperclip className="h-4 w-4" />
|
) : (
|
||||||
)}
|
<Paperclip className="h-4 w-4" />
|
||||||
<span className="text-sm">Attach</span>
|
)}
|
||||||
</Button>
|
<span className="text-sm">Attach</span>
|
||||||
|
</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