mirror of https://github.com/kortix-ai/suna.git
config, alerts
This commit is contained in:
parent
7325559d26
commit
7e72fa4f3f
|
@ -7,8 +7,8 @@ import { usePathname } from "next/navigation";
|
|||
export default function PersonalAccountSettingsPage({children}: {children: React.ReactNode}) {
|
||||
const pathname = usePathname();
|
||||
const items = [
|
||||
{ name: "Profile", href: "/settings" },
|
||||
{ name: "Teams", href: "/settings/teams" },
|
||||
// { name: "Profile", href: "/settings" },
|
||||
// { name: "Teams", href: "/settings/teams" },
|
||||
{ name: "Billing", href: "/settings/billing" },
|
||||
]
|
||||
return (
|
||||
|
|
|
@ -19,9 +19,10 @@ import { useAgentStream } from '@/hooks/useAgentStream';
|
|||
import { Markdown } from '@/components/ui/markdown';
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { BillingErrorAlert } from '@/components/billing/BillingErrorAlert';
|
||||
import { SUBSCRIPTION_PLANS } from '@/components/billing/PlanComparison';
|
||||
import { BillingErrorAlert } from '@/components/billing/usage-limit-alert';
|
||||
import { SUBSCRIPTION_PLANS } from '@/components/billing/plan-comparison';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { isLocalMode } from "@/lib/config";
|
||||
|
||||
import { UnifiedMessage, ParsedContent, ParsedMetadata, ThreadParams } from '@/components/thread/types';
|
||||
import { getToolIcon, extractPrimaryParam, safeJsonParse } from '@/components/thread/utils';
|
||||
|
@ -1069,6 +1070,12 @@ export default function ThreadPage({ params }: { params: Promise<ThreadParams> }
|
|||
|
||||
// Check billing status when agent completes
|
||||
const checkBillingStatus = useCallback(async () => {
|
||||
// Skip billing checks in local development mode
|
||||
if (isLocalMode()) {
|
||||
console.log("Running in local development mode - billing checks are disabled");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!project?.account_id) return;
|
||||
|
||||
const supabase = createClient();
|
||||
|
|
|
@ -5,14 +5,16 @@ import { Skeleton } from "@/components/ui/skeleton";
|
|||
import { useRouter } from 'next/navigation';
|
||||
import { Menu } from "lucide-react";
|
||||
import { ChatInput, ChatInputHandles } from '@/components/thread/chat-input';
|
||||
import { initiateAgent } from "@/lib/api";
|
||||
import { initiateAgent, createThread, addUserMessage, startAgent } from "@/lib/api";
|
||||
import { useIsMobile } from "@/hooks/use-mobile";
|
||||
import { useSidebar } from "@/components/ui/sidebar";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";
|
||||
import { useBillingError } from "@/hooks/useBillingError";
|
||||
import { BillingErrorAlert } from "@/components/billing/BillingErrorAlert";
|
||||
import { BillingErrorAlert } from "@/components/billing/usage-limit-alert";
|
||||
import { useAccounts } from "@/hooks/use-accounts";
|
||||
import { isLocalMode } from "@/lib/config";
|
||||
import { toast } from "sonner";
|
||||
|
||||
// Constant for localStorage key to ensure consistency
|
||||
const PENDING_PROMPT_KEY = 'pendingAgentPrompt';
|
||||
|
@ -35,113 +37,139 @@ function DashboardContent() {
|
|||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
// Get any pending files
|
||||
const pendingFiles = chatInputRef.current?.getPendingFiles() || [];
|
||||
// Check if any files are attached
|
||||
const files = chatInputRef.current?.getPendingFiles() || [];
|
||||
|
||||
// Create FormData for the request
|
||||
const formData = new FormData();
|
||||
formData.append('prompt', message.trim());
|
||||
|
||||
// Add model options
|
||||
formData.append('model_name', options?.model_name || 'anthropic/claude-3-7-sonnet-latest');
|
||||
formData.append('enable_thinking', options?.enable_thinking ? 'true' : 'false');
|
||||
|
||||
// Add files if any
|
||||
pendingFiles.forEach(file => {
|
||||
formData.append('files', file, file.name);
|
||||
});
|
||||
|
||||
// Single API call to initialize everything
|
||||
const response = await initiateAgent(formData);
|
||||
|
||||
// Clear pendingFiles
|
||||
chatInputRef.current?.clearPendingFiles();
|
||||
|
||||
// Clear pending prompt from localStorage
|
||||
// Clear localStorage if this is a successful submission
|
||||
localStorage.removeItem(PENDING_PROMPT_KEY);
|
||||
|
||||
// Navigate to the new thread
|
||||
router.push(`/agents/${response.thread_id}`);
|
||||
|
||||
} catch (error: any) {
|
||||
// Check specifically for billing errors (402 Payment Required)
|
||||
if (error.message?.includes('(402)') || error?.status === 402) {
|
||||
console.log("Billing error detected:", error);
|
||||
if (files.length > 0) {
|
||||
// Create a FormData instance
|
||||
const formData = new FormData();
|
||||
|
||||
// Try to extract the error details from the error object
|
||||
try {
|
||||
// Try to parse the error.response or the error itself
|
||||
let errorDetails;
|
||||
|
||||
// First attempt: check if error.data exists and has a detail property
|
||||
if (error.data?.detail) {
|
||||
errorDetails = error.data.detail;
|
||||
console.log("Extracted billing error details from error.data.detail:", errorDetails);
|
||||
}
|
||||
// Second attempt: check if error.detail exists directly
|
||||
else if (error.detail) {
|
||||
errorDetails = error.detail;
|
||||
console.log("Extracted billing error details from error.detail:", errorDetails);
|
||||
}
|
||||
// Third attempt: try to parse the error text if it's JSON
|
||||
else if (typeof error.text === 'function') {
|
||||
const text = await error.text();
|
||||
console.log("Extracted error text:", text);
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
errorDetails = parsed.detail || parsed;
|
||||
console.log("Parsed error text as JSON:", errorDetails);
|
||||
} catch (e) {
|
||||
// Not JSON, use regex to extract info
|
||||
console.log("Error text is not valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have details, try to extract from the error message
|
||||
if (!errorDetails && error.message) {
|
||||
const match = error.message.match(/Monthly limit of (\d+) minutes reached/);
|
||||
if (match) {
|
||||
const minutes = parseInt(match[1]);
|
||||
errorDetails = {
|
||||
message: error.message,
|
||||
subscription: {
|
||||
price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier by default
|
||||
plan_name: "Free",
|
||||
current_usage: minutes / 60, // Convert to hours
|
||||
limit: minutes / 60 // Convert to hours
|
||||
}
|
||||
};
|
||||
console.log("Extracted billing error details from error message:", errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the billing error with the details we extracted
|
||||
if (errorDetails) {
|
||||
console.log("Handling billing error with extracted details:", errorDetails);
|
||||
handleBillingError(errorDetails);
|
||||
} else {
|
||||
// Fallback with generic billing error
|
||||
console.log("Using fallback generic billing error");
|
||||
handleBillingError({
|
||||
message: "You've reached your monthly usage limit. Please upgrade your plan.",
|
||||
subscription: {
|
||||
price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier
|
||||
plan_name: "Free"
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error("Error parsing billing error details:", parseError);
|
||||
// Fallback with generic error
|
||||
handleBillingError({
|
||||
message: "You've reached your monthly usage limit. Please upgrade your plan."
|
||||
});
|
||||
// Append the message
|
||||
formData.append('message', message);
|
||||
|
||||
// Append all files
|
||||
files.forEach(file => {
|
||||
formData.append('files', file);
|
||||
});
|
||||
|
||||
// Add any additional options
|
||||
if (options) {
|
||||
formData.append('options', JSON.stringify(options));
|
||||
}
|
||||
|
||||
// Don't rethrow - we've handled this error with the billing alert
|
||||
setIsSubmitting(false);
|
||||
return; // Exit handleSubmit
|
||||
// Call initiateAgent API
|
||||
const result = await initiateAgent(formData);
|
||||
console.log('Agent initiated:', result);
|
||||
|
||||
// Navigate to the thread
|
||||
if (result.thread_id) {
|
||||
router.push(`/agents/${result.thread_id}`);
|
||||
}
|
||||
} else {
|
||||
// For text-only messages, first create a thread
|
||||
const thread = await createThread("");
|
||||
|
||||
// Then add the user message
|
||||
await addUserMessage(thread.thread_id, message);
|
||||
|
||||
// Start the agent on this thread with the options
|
||||
await startAgent(thread.thread_id, options);
|
||||
|
||||
// Navigate to thread
|
||||
router.push(`/agents/${thread.thread_id}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('Error creating thread or initiating agent:', error);
|
||||
|
||||
// Skip billing error checks in local development mode
|
||||
if (isLocalMode()) {
|
||||
console.log("Running in local development mode - billing checks are disabled");
|
||||
} else {
|
||||
// Check specifically for billing errors (402 Payment Required)
|
||||
if (error.message?.includes('(402)') || error?.status === 402) {
|
||||
console.log("Billing error detected:", error);
|
||||
|
||||
// Try to extract the error details from the error object
|
||||
try {
|
||||
// Try to parse the error.response or the error itself
|
||||
let errorDetails;
|
||||
|
||||
// First attempt: check if error.data exists and has a detail property
|
||||
if (error.data?.detail) {
|
||||
errorDetails = error.data.detail;
|
||||
console.log("Extracted billing error details from error.data.detail:", errorDetails);
|
||||
}
|
||||
// Second attempt: check if error.detail exists directly
|
||||
else if (error.detail) {
|
||||
errorDetails = error.detail;
|
||||
console.log("Extracted billing error details from error.detail:", errorDetails);
|
||||
}
|
||||
// Third attempt: try to parse the error text if it's JSON
|
||||
else if (typeof error.text === 'function') {
|
||||
const text = await error.text();
|
||||
console.log("Extracted error text:", text);
|
||||
try {
|
||||
const parsed = JSON.parse(text);
|
||||
errorDetails = parsed.detail || parsed;
|
||||
console.log("Parsed error text as JSON:", errorDetails);
|
||||
} catch (e) {
|
||||
// Not JSON, use regex to extract info
|
||||
console.log("Error text is not valid JSON");
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have details, try to extract from the error message
|
||||
if (!errorDetails && error.message) {
|
||||
const match = error.message.match(/Monthly limit of (\d+) minutes reached/);
|
||||
if (match) {
|
||||
const minutes = parseInt(match[1]);
|
||||
errorDetails = {
|
||||
message: error.message,
|
||||
subscription: {
|
||||
price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier by default
|
||||
plan_name: "Free",
|
||||
current_usage: minutes / 60, // Convert to hours
|
||||
limit: minutes / 60 // Convert to hours
|
||||
}
|
||||
};
|
||||
console.log("Extracted billing error details from error message:", errorDetails);
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the billing error with the details we extracted
|
||||
if (errorDetails) {
|
||||
console.log("Handling billing error with extracted details:", errorDetails);
|
||||
handleBillingError(errorDetails);
|
||||
} else {
|
||||
// Fallback with generic billing error
|
||||
console.log("Using fallback generic billing error");
|
||||
handleBillingError({
|
||||
message: "You've reached your monthly usage limit. Please upgrade your plan.",
|
||||
subscription: {
|
||||
price_id: "price_1RGJ9GG6l1KZGqIroxSqgphC", // Free tier
|
||||
plan_name: "Free"
|
||||
}
|
||||
});
|
||||
}
|
||||
} catch (parseError) {
|
||||
console.error("Error parsing billing error details:", parseError);
|
||||
// Fallback with generic error
|
||||
handleBillingError({
|
||||
message: "You've reached your monthly usage limit. Please upgrade your plan."
|
||||
});
|
||||
}
|
||||
|
||||
// Don't rethrow - we've handled this error with the billing alert
|
||||
setIsSubmitting(false);
|
||||
return; // Exit handleSubmit
|
||||
}
|
||||
}
|
||||
|
||||
// Handle other errors or rethrow
|
||||
toast.error(error.message || "An error occurred");
|
||||
|
||||
console.error("Error creating agent:", error);
|
||||
setIsSubmitting(false);
|
||||
|
|
|
@ -6,6 +6,7 @@ import {
|
|||
SidebarInset,
|
||||
SidebarProvider,
|
||||
} from "@/components/ui/sidebar"
|
||||
import { PricingAlert } from "@/components/billing/pricing-alert"
|
||||
import { MaintenanceAlert } from "@/components/maintenance-alert"
|
||||
import { useAccounts } from "@/hooks/use-accounts"
|
||||
|
||||
|
@ -16,13 +17,14 @@ interface DashboardLayoutProps {
|
|||
export default function DashboardLayout({
|
||||
children,
|
||||
}: DashboardLayoutProps) {
|
||||
const [showPricingAlert, setShowPricingAlert] = useState(false)
|
||||
const [showMaintenanceAlert, setShowMaintenanceAlert] = useState(false)
|
||||
const { data: accounts } = useAccounts()
|
||||
const personalAccount = accounts?.find(account => account.personal_account)
|
||||
|
||||
useEffect(() => {
|
||||
// Show the maintenance alert when component mounts
|
||||
setShowMaintenanceAlert(true)
|
||||
setShowPricingAlert(true)
|
||||
setShowMaintenanceAlert(false)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
|
@ -34,12 +36,18 @@ export default function DashboardLayout({
|
|||
</div>
|
||||
</SidebarInset>
|
||||
|
||||
<MaintenanceAlert
|
||||
open={showMaintenanceAlert}
|
||||
onOpenChange={setShowMaintenanceAlert}
|
||||
<PricingAlert
|
||||
open={showPricingAlert}
|
||||
onOpenChange={setShowPricingAlert}
|
||||
closeable={true}
|
||||
accountId={personalAccount?.account_id}
|
||||
/>
|
||||
|
||||
<MaintenanceAlert
|
||||
open={showMaintenanceAlert}
|
||||
onOpenChange={setShowMaintenanceAlert}
|
||||
closeable={true}
|
||||
/>
|
||||
</SidebarProvider>
|
||||
)
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import { createClient } from "@/lib/supabase/server";
|
||||
import { SubmitButton } from "../ui/submit-button";
|
||||
import { manageSubscription } from "@/lib/actions/billing";
|
||||
import { PlanComparison, SUBSCRIPTION_PLANS } from "../billing/PlanComparison";
|
||||
import { PlanComparison, SUBSCRIPTION_PLANS } from "../billing/plan-comparison";
|
||||
import { isLocalMode } from "@/lib/config";
|
||||
|
||||
type Props = {
|
||||
accountId: string;
|
||||
|
@ -9,87 +10,141 @@ type Props = {
|
|||
}
|
||||
|
||||
export default async function AccountBillingStatus({ accountId, returnUrl }: Props) {
|
||||
// In local development mode, show a simplified component
|
||||
if (isLocalMode()) {
|
||||
return (
|
||||
<div className="rounded-xl border shadow-sm bg-card p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Billing Status</h2>
|
||||
<div className="p-4 mb-4 bg-muted/30 border border-border rounded-lg text-center">
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Running in local development mode - billing features are disabled
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
Agent usage limits are not enforced in this environment
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const supabaseClient = await createClient();
|
||||
|
||||
const { data: billingData, error: billingError } = await supabaseClient.functions.invoke('billing-functions', {
|
||||
body: {
|
||||
action: "get_billing_status",
|
||||
args: {
|
||||
account_id: accountId
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Get current subscription details
|
||||
|
||||
// Get account subscription and usage data
|
||||
const { data: subscriptionData } = await supabaseClient
|
||||
.schema('basejump')
|
||||
.from('billing_subscriptions')
|
||||
.select('price_id')
|
||||
.select('*')
|
||||
.eq('account_id', accountId)
|
||||
.eq('status', 'active')
|
||||
.limit(1)
|
||||
.order('created_at', { ascending: false })
|
||||
.single();
|
||||
|
||||
const currentPlanId = subscriptionData?.price_id;
|
||||
|
||||
// Get agent run hours for current month
|
||||
const startOfMonth = new Date();
|
||||
startOfMonth.setDate(1);
|
||||
startOfMonth.setHours(0, 0, 0, 0);
|
||||
// First get threads for this account
|
||||
const { data: threadsData } = await supabaseClient
|
||||
|
||||
// Get agent runs for this account
|
||||
// Get the account's threads
|
||||
const { data: threads } = await supabaseClient
|
||||
.from('threads')
|
||||
.select('thread_id')
|
||||
.eq('account_id', accountId);
|
||||
|
||||
const threadIds = threadsData?.map(t => t.thread_id) || [];
|
||||
|
||||
// Then get agent runs for those threads
|
||||
const { data: agentRunData, error: agentRunError } = await supabaseClient
|
||||
.from('agent_runs')
|
||||
.select('started_at, completed_at')
|
||||
.in('thread_id', threadIds)
|
||||
.gte('started_at', startOfMonth.toISOString());
|
||||
|
||||
let totalSeconds = 0;
|
||||
if (agentRunData) {
|
||||
totalSeconds = agentRunData.reduce((acc, run) => {
|
||||
const start = new Date(run.started_at);
|
||||
const end = run.completed_at ? new Date(run.completed_at) : new Date();
|
||||
const seconds = (end.getTime() - start.getTime()) / 1000;
|
||||
return acc + seconds;
|
||||
}, 0);
|
||||
|
||||
const threadIds = threads?.map(t => t.thread_id) || [];
|
||||
|
||||
// Get current month usage
|
||||
const now = new Date();
|
||||
const startOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
const isoStartOfMonth = startOfMonth.toISOString();
|
||||
|
||||
let totalAgentTime = 0;
|
||||
let usageDisplay = "No usage this month";
|
||||
|
||||
if (threadIds.length > 0) {
|
||||
const { data: agentRuns } = await supabaseClient
|
||||
.from('agent_runs')
|
||||
.select('started_at, completed_at')
|
||||
.in('thread_id', threadIds)
|
||||
.gte('started_at', isoStartOfMonth);
|
||||
|
||||
if (agentRuns && agentRuns.length > 0) {
|
||||
const nowTimestamp = now.getTime();
|
||||
|
||||
totalAgentTime = agentRuns.reduce((total, run) => {
|
||||
const startTime = new Date(run.started_at).getTime();
|
||||
const endTime = run.completed_at
|
||||
? new Date(run.completed_at).getTime()
|
||||
: nowTimestamp;
|
||||
|
||||
return total + (endTime - startTime) / 1000; // In seconds
|
||||
}, 0);
|
||||
|
||||
// Convert to minutes
|
||||
const totalMinutes = Math.round(totalAgentTime / 60);
|
||||
usageDisplay = `${totalMinutes} minutes`;
|
||||
}
|
||||
}
|
||||
|
||||
const hours = Math.floor(totalSeconds / 3600);
|
||||
const minutes = Math.floor((totalSeconds % 3600) / 60);
|
||||
const seconds = Math.floor(totalSeconds % 60);
|
||||
const usageDisplay = `${hours}h ${minutes}m ${seconds}s`;
|
||||
|
||||
const isPlan = (planId?: string) => {
|
||||
return subscriptionData?.price_id === planId;
|
||||
};
|
||||
|
||||
const planName = isPlan(SUBSCRIPTION_PLANS.FREE)
|
||||
? "Free"
|
||||
: isPlan(SUBSCRIPTION_PLANS.PRO)
|
||||
? "Pro"
|
||||
: isPlan(SUBSCRIPTION_PLANS.ENTERPRISE)
|
||||
? "Enterprise"
|
||||
: "Unknown";
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{!Boolean(billingData?.billing_enabled) ? (
|
||||
<div className="rounded-xl bg-destructive/10 border border-destructive p-6">
|
||||
<h3 className="text-lg font-medium text-destructive mb-2">Billing Not Enabled</h3>
|
||||
<p className="text-sm text-destructive/80">
|
||||
Billing is not enabled for this account. Check out usebasejump.com for more info or remove this component if you don't plan on enabling billing.
|
||||
</p>
|
||||
</div>
|
||||
<div className="rounded-xl border shadow-sm bg-card p-6">
|
||||
<h2 className="text-xl font-semibold mb-4">Billing Status</h2>
|
||||
|
||||
{subscriptionData ? (
|
||||
<>
|
||||
<div className="mb-6">
|
||||
<div className="rounded-lg border bg-background p-4 grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div>
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground/90">Current Plan</span>
|
||||
<span className="text-sm font-medium text-card-title">{planName}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground/90">Agent Usage This Month</span>
|
||||
<span className="text-sm font-medium text-card-title">{usageDisplay}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Plans Comparison */}
|
||||
<PlanComparison
|
||||
accountId={accountId}
|
||||
returnUrl={returnUrl}
|
||||
className="mb-6"
|
||||
/>
|
||||
|
||||
{/* Manage Subscription Button */}
|
||||
<form>
|
||||
<input type="hidden" name="accountId" value={accountId} />
|
||||
<input type="hidden" name="returnUrl" value={returnUrl} />
|
||||
<SubmitButton
|
||||
pendingText="Loading..."
|
||||
formAction={manageSubscription}
|
||||
className="w-full bg-primary text-white hover:bg-primary/90 shadow-md hover:shadow-lg transition-all"
|
||||
>
|
||||
Manage Subscription
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<div className="rounded-xl bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] border border-border p-6">
|
||||
<div className="flex flex-col gap-4">
|
||||
<div className="mb-6">
|
||||
<div className="rounded-lg border bg-background p-4 gap-4">
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground/90">Status</span>
|
||||
<span className="text-sm font-medium text-card-title">
|
||||
{(!currentPlanId || currentPlanId === SUBSCRIPTION_PLANS.FREE) ? 'Active (Free)' : billingData.status === 'active' ? 'Active' : 'Inactive'}
|
||||
</span>
|
||||
<span className="text-sm font-medium text-foreground/90">Current Plan</span>
|
||||
<span className="text-sm font-medium text-card-title">Free</span>
|
||||
</div>
|
||||
{billingData.plan_name && (
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground/90">Plan</span>
|
||||
<span className="text-sm font-medium text-card-title">{billingData.plan_name}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex justify-between items-center">
|
||||
<span className="text-sm font-medium text-foreground/90">Agent Usage This Month</span>
|
||||
<span className="text-sm font-medium text-card-title">{usageDisplay}</span>
|
||||
|
|
|
@ -57,9 +57,9 @@ export default function ClientUserAccountButton({
|
|||
<DropdownMenuItem asChild className="rounded-md hover:bg-hover-bg cursor-pointer">
|
||||
<Link href="/dashboard" className="flex w-full h-full text-foreground/90">My Account</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild className="rounded-md hover:bg-hover-bg cursor-pointer">
|
||||
{/* <DropdownMenuItem asChild className="rounded-md hover:bg-hover-bg cursor-pointer">
|
||||
<Link href="/settings" className="flex w-full h-full text-foreground/90">Settings</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem asChild className="rounded-md hover:bg-hover-bg cursor-pointer">
|
||||
<Link href="/settings/teams" className="flex w-full h-full text-foreground/90">Teams</Link>
|
||||
</DropdownMenuItem>
|
||||
|
|
|
@ -8,6 +8,7 @@ import { setupNewSubscription } from "@/lib/actions/billing";
|
|||
import { SubmitButton } from "@/components/ui/submit-button";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { siteConfig } from "@/lib/home";
|
||||
import { isLocalMode } from "@/lib/config";
|
||||
|
||||
// Create SUBSCRIPTION_PLANS using stripePriceId from siteConfig
|
||||
export const SUBSCRIPTION_PLANS = {
|
||||
|
@ -16,15 +17,6 @@ export const SUBSCRIPTION_PLANS = {
|
|||
ENTERPRISE: siteConfig.cloudPricingItems.find(item => item.name === 'Enterprise')?.stripePriceId || '',
|
||||
};
|
||||
|
||||
interface PlanComparisonProps {
|
||||
accountId?: string | null;
|
||||
returnUrl?: string;
|
||||
isManaged?: boolean;
|
||||
onPlanSelect?: (planId: string) => void;
|
||||
className?: string;
|
||||
isCompact?: boolean; // When true, uses vertical stacked layout for modals
|
||||
}
|
||||
|
||||
// Price display animation component
|
||||
const PriceDisplay = ({ tier, isCompact }: { tier: typeof siteConfig.cloudPricingItems[number]; isCompact?: boolean }) => {
|
||||
return (
|
||||
|
@ -44,6 +36,15 @@ const PriceDisplay = ({ tier, isCompact }: { tier: typeof siteConfig.cloudPricin
|
|||
);
|
||||
};
|
||||
|
||||
interface PlanComparisonProps {
|
||||
accountId?: string | null;
|
||||
returnUrl?: string;
|
||||
isManaged?: boolean;
|
||||
onPlanSelect?: (planId: string) => void;
|
||||
className?: string;
|
||||
isCompact?: boolean; // When true, uses vertical stacked layout for modals
|
||||
}
|
||||
|
||||
export function PlanComparison({
|
||||
accountId,
|
||||
returnUrl = typeof window !== 'undefined' ? window.location.href : '',
|
||||
|
@ -75,6 +76,17 @@ export function PlanComparison({
|
|||
fetchCurrentPlan();
|
||||
}, [accountId]);
|
||||
|
||||
// For local development mode, show a message instead
|
||||
if (isLocalMode()) {
|
||||
return (
|
||||
<div className={cn("p-4 bg-muted/30 border border-border rounded-lg text-center", className)}>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
Running in local development mode - billing features are disabled
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
|
@ -0,0 +1,211 @@
|
|||
"use client"
|
||||
|
||||
import { X, Zap, Github, Check } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Portal } from "@/components/ui/portal"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { setupNewSubscription } from "@/lib/actions/billing"
|
||||
import { SubmitButton } from "@/components/ui/submit-button"
|
||||
import { siteConfig } from "@/lib/home"
|
||||
import { isLocalMode } from "@/lib/config"
|
||||
|
||||
interface PricingAlertProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
closeable?: boolean
|
||||
accountId?: string | null | undefined
|
||||
}
|
||||
|
||||
export function PricingAlert({ open, onOpenChange, closeable = true, accountId }: PricingAlertProps) {
|
||||
const returnUrl = typeof window !== 'undefined' ? window.location.href : '';
|
||||
|
||||
// Skip rendering in local development mode
|
||||
if (isLocalMode() || !open) return null;
|
||||
|
||||
// Filter plans to show only Pro and Enterprise
|
||||
const premiumPlans = siteConfig.cloudPricingItems.filter(plan =>
|
||||
plan.name === 'Pro' || plan.name === 'Enterprise'
|
||||
);
|
||||
|
||||
return (
|
||||
<Portal>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center overflow-y-auto py-8 px-4"
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/60 backdrop-blur-sm"
|
||||
onClick={closeable ? () => onOpenChange(false) : undefined}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
className={cn(
|
||||
"relative bg-background rounded-xl shadow-2xl w-full max-w-3xl mx-3 border border-border"
|
||||
)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="pricing-modal-title"
|
||||
>
|
||||
<div className="p-6">
|
||||
{/* Close button */}
|
||||
{closeable && (
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="absolute top-4 right-4 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<X className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center mb-8">
|
||||
<div className="inline-flex items-center justify-center p-2 bg-primary/10 rounded-full mb-3">
|
||||
<Zap className="h-5 w-5 text-primary" />
|
||||
</div>
|
||||
<h2 id="pricing-modal-title" className="text-2xl font-medium tracking-tight mb-2">
|
||||
Choose Your Suna Experience
|
||||
</h2>
|
||||
<p className="text-muted-foreground max-w-lg mx-auto">
|
||||
Due to overwhelming demand and AI costs, we're currently focusing on delivering
|
||||
our best experience to dedicated users. Select your preferred option below.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Plan comparison - 3 column layout */}
|
||||
<div className="grid md:grid-cols-3 gap-4 mb-6">
|
||||
{/* Self-Host Option */}
|
||||
<div className="rounded-xl bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] border border-border hover:border-muted-foreground/30 transition-all duration-300">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<p className="text-sm flex items-center">Open Source</p>
|
||||
<div className="flex items-baseline mt-2">
|
||||
<span className="text-2xl font-semibold">Self-host</span>
|
||||
</div>
|
||||
<p className="text-sm mt-2">Full control with your own infrastructure</p>
|
||||
<div className="inline-flex 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">
|
||||
∞ hours / month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<Check className="h-4 w-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-muted-foreground">No usage limitations</span>
|
||||
</div>
|
||||
<Link
|
||||
href="https://github.com/kortix-ai/suna"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="h-10 w-full flex items-center justify-center gap-2 text-sm font-normal tracking-wide rounded-full px-4 cursor-pointer transition-all ease-out active:scale-95 bg-secondary/10 text-secondary shadow-[0px_1px_2px_0px_rgba(255,255,255,0.16)_inset,0px_3px_3px_-1.5px_rgba(16,24,40,0.24),0px_1px_1px_-0.5px_rgba(16,24,40,0.20)]"
|
||||
>
|
||||
<Github className="h-4 w-4" />
|
||||
<span>View on GitHub</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Pro Plan */}
|
||||
<div className="rounded-xl 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 relative transform hover:scale-105 transition-all duration-300">
|
||||
<div className="absolute -top-3 -right-3">
|
||||
<span className="bg-gradient-to-b from-secondary/50 from-[1.92%] to-secondary to-[100%] text-white h-6 inline-flex w-fit items-center justify-center px-3 rounded-full text-xs font-medium shadow-[0px_6px_6px_-3px_rgba(0,0,0,0.08),0px_3px_3px_-1.5px_rgba(0,0,0,0.08),0px_1px_1px_-0.5px_rgba(0,0,0,0.08),0px_0px_0px_1px_rgba(255,255,255,0.12)_inset,0px_1px_0px_0px_rgba(255,255,255,0.12)_inset]">
|
||||
Most Popular
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<p className="text-sm flex items-center font-medium">Pro</p>
|
||||
<div className="flex items-baseline mt-2">
|
||||
<span className="text-2xl font-semibold">{premiumPlans[0]?.price || "$19"}</span>
|
||||
<span className="ml-2">/month</span>
|
||||
</div>
|
||||
<p className="text-sm mt-2">Supercharge your productivity with {premiumPlans[0]?.hours || "500 hours"} of Suna</p>
|
||||
<div className="inline-flex 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">
|
||||
{premiumPlans[0]?.hours || "500 hours"}/month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<Check className="h-4 w-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-muted-foreground">Perfect for individuals and small teams</span>
|
||||
</div>
|
||||
<form>
|
||||
<input type="hidden" name="accountId" value={accountId || ''} />
|
||||
<input type="hidden" name="returnUrl" value={returnUrl} />
|
||||
<input type="hidden" name="planId" value={
|
||||
premiumPlans[0]?.stripePriceId || ''
|
||||
} />
|
||||
<SubmitButton
|
||||
pendingText="..."
|
||||
formAction={setupNewSubscription}
|
||||
className="h-10 w-full flex items-center justify-center text-sm font-medium tracking-wide rounded-full px-4 cursor-pointer transition-all ease-out active:scale-95 bg-primary text-primary-foreground shadow-[inset_0_1px_2px_rgba(255,255,255,0.25),0_3px_3px_-1.5px_rgba(16,24,40,0.06),0_1px_1px_rgba(16,24,40,0.08)]"
|
||||
>
|
||||
Get Started Now
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Enterprise Plan */}
|
||||
<div className="rounded-xl bg-[#F3F4F6] dark:bg-[#F9FAFB]/[0.02] border border-border hover:border-muted-foreground/30 transition-all duration-300">
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<p className="text-sm flex items-center font-medium">Enterprise</p>
|
||||
<div className="flex items-baseline mt-2">
|
||||
<span className="text-2xl font-semibold">{premiumPlans[1]?.price || "$99"}</span>
|
||||
<span className="ml-2">/month</span>
|
||||
</div>
|
||||
<p className="text-sm mt-2">Unlock boundless potential with {premiumPlans[1]?.hours || "2000 hours"} of Suna</p>
|
||||
<div className="inline-flex 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">
|
||||
{premiumPlans[1]?.hours || "2000 hours"}/month
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="px-4 pb-4">
|
||||
<div className="flex items-start gap-2 mb-3">
|
||||
<Check className="h-4 w-4 text-green-500 flex-shrink-0 mt-0.5" />
|
||||
<span className="text-xs text-muted-foreground">Ideal for larger organizations and power users</span>
|
||||
</div>
|
||||
<form>
|
||||
<input type="hidden" name="accountId" value={accountId || ''} />
|
||||
<input type="hidden" name="returnUrl" value={returnUrl} />
|
||||
<input type="hidden" name="planId" value={
|
||||
premiumPlans[1]?.stripePriceId || ''
|
||||
} />
|
||||
<SubmitButton
|
||||
pendingText="..."
|
||||
formAction={setupNewSubscription}
|
||||
className="h-10 w-full flex items-center justify-center text-sm font-normal tracking-wide rounded-full px-4 cursor-pointer transition-all ease-out active:scale-95 bg-gradient-to-b from-secondary/50 from-[1.92%] to-secondary to-[100%] text-white shadow-[0px_1px_2px_0px_rgba(255,255,255,0.16)_inset,0px_3px_3px_-1.5px_rgba(16,24,40,0.24),0px_1px_1px_-0.5px_rgba(16,24,40,0.20)]"
|
||||
>
|
||||
Upgrade to Enterprise
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Portal>
|
||||
)
|
||||
}
|
|
@ -1,9 +1,10 @@
|
|||
import { AlertCircle, X } from "lucide-react";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { Portal } from "@/components/ui/portal";
|
||||
import { PlanComparison } from "./PlanComparison";
|
||||
import { PlanComparison } from "./plan-comparison";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { motion, AnimatePresence } from "motion/react";
|
||||
import { isLocalMode } from "@/lib/config";
|
||||
|
||||
interface BillingErrorAlertProps {
|
||||
message?: string;
|
||||
|
@ -25,8 +26,9 @@ export function BillingErrorAlert({
|
|||
isOpen
|
||||
}: BillingErrorAlertProps) {
|
||||
const returnUrl = typeof window !== 'undefined' ? window.location.href : '';
|
||||
|
||||
if (!isOpen) return null;
|
||||
|
||||
// Skip rendering in local development mode
|
||||
if (isLocalMode() || !isOpen) return null;
|
||||
|
||||
return (
|
||||
<Portal>
|
|
@ -1,149 +1,94 @@
|
|||
"use client"
|
||||
|
||||
import { AlertDialog, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
|
||||
import { AlertCircle, X, Zap, Github } from "lucide-react"
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from "@/components/ui/alert-dialog"
|
||||
import { Clock, Github, X } from "lucide-react"
|
||||
import Link from "next/link"
|
||||
import { AnimatePresence, motion } from "motion/react"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { Portal } from "@/components/ui/portal"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { setupNewSubscription } from "@/lib/actions/billing"
|
||||
import { SubmitButton } from "@/components/ui/submit-button"
|
||||
import { siteConfig } from "@/lib/home"
|
||||
|
||||
interface MaintenanceAlertProps {
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
closeable?: boolean
|
||||
accountId?: string | null | undefined
|
||||
}
|
||||
|
||||
export function MaintenanceAlert({ open, onOpenChange, closeable = true, accountId }: MaintenanceAlertProps) {
|
||||
const returnUrl = typeof window !== 'undefined' ? window.location.href : '';
|
||||
|
||||
if (!open) return null;
|
||||
|
||||
// Filter plans to show only Pro and Enterprise
|
||||
const premiumPlans = siteConfig.cloudPricingItems.filter(plan =>
|
||||
plan.name === 'Pro' || plan.name === 'Enterprise'
|
||||
);
|
||||
|
||||
export function MaintenanceAlert({ open, onOpenChange, closeable = true }: MaintenanceAlertProps) {
|
||||
return (
|
||||
<Portal>
|
||||
<AnimatePresence>
|
||||
{open && (
|
||||
<>
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 z-[9999] flex items-center justify-center overflow-y-auto py-4"
|
||||
<AlertDialog open={open} onOpenChange={closeable ? onOpenChange : undefined}>
|
||||
<AlertDialogContent className="max-w-2xl w-[90vw] p-0 border-0 shadow-lg overflow-hidden rounded-2xl z-[9999]">
|
||||
<motion.div
|
||||
className="relative"
|
||||
initial={{ opacity: 0, scale: 0.95 }}
|
||||
animate={{ opacity: 1, scale: 1 }}
|
||||
transition={{ type: "spring", damping: 30, stiffness: 300 }}
|
||||
>
|
||||
{/* Background pattern */}
|
||||
<div className="absolute inset-0 bg-accent/20 opacity-20">
|
||||
<div className="absolute inset-0 bg-grid-white/10 [mask-image:radial-gradient(white,transparent_85%)]" />
|
||||
</div>
|
||||
|
||||
{closeable && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="absolute right-4 top-4 z-20 rounded-full hover:bg-background/80"
|
||||
onClick={() => onOpenChange(false)}
|
||||
>
|
||||
{/* Backdrop */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0 }}
|
||||
animate={{ opacity: 1 }}
|
||||
exit={{ opacity: 0 }}
|
||||
transition={{ duration: 0.2 }}
|
||||
className="fixed inset-0 bg-black/40 backdrop-blur-sm"
|
||||
onClick={closeable ? () => onOpenChange(false) : undefined}
|
||||
aria-hidden="true"
|
||||
/>
|
||||
|
||||
{/* Modal */}
|
||||
<motion.div
|
||||
initial={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
animate={{ opacity: 1, scale: 1, y: 0 }}
|
||||
exit={{ opacity: 0, scale: 0.95, y: 20 }}
|
||||
transition={{ duration: 0.2, ease: [0.4, 0, 0.2, 1] }}
|
||||
className={cn(
|
||||
"relative bg-background rounded-lg shadow-xl w-full max-w-md mx-3"
|
||||
)}
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="free-tier-modal-title"
|
||||
>
|
||||
<div className="p-4">
|
||||
{/* Close button */}
|
||||
{closeable && (
|
||||
<button
|
||||
onClick={() => onOpenChange(false)}
|
||||
className="absolute top-2 right-2 text-muted-foreground hover:text-foreground transition-colors"
|
||||
aria-label="Close dialog"
|
||||
>
|
||||
<X className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
{/* Header */}
|
||||
<div className="text-center mb-4">
|
||||
<div className="inline-flex items-center justify-center p-1.5 bg-primary/10 rounded-full mb-2">
|
||||
<Zap className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<h2 id="free-tier-modal-title" className="text-lg font-medium tracking-tight mb-1">
|
||||
Free Tier Unavailable At This Time
|
||||
</h2>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Due to extremely high demand, we cannot offer a free tier at the moment. Upgrade to Pro to continue using our service.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Custom plan comparison wrapper to show Pro, Enterprise and Self-Host side by side */}
|
||||
<div className="grid grid-cols-3 gap-2 mb-3">
|
||||
{premiumPlans.map((tier) => (
|
||||
<div key={tier.name} className="border border-border rounded-lg p-3">
|
||||
<div className="text-center mb-2">
|
||||
<h3 className="font-medium">{tier.name}</h3>
|
||||
<p className="text-sm font-bold">{tier.price}/mo</p>
|
||||
<p className="text-xs text-muted-foreground">{tier.hours}/month</p>
|
||||
</div>
|
||||
<form>
|
||||
<input type="hidden" name="accountId" value={accountId || ''} />
|
||||
<input type="hidden" name="returnUrl" value={returnUrl} />
|
||||
<input type="hidden" name="planId" value={
|
||||
tier.name === 'Pro'
|
||||
? siteConfig.cloudPricingItems.find(item => item.name === 'Pro')?.stripePriceId || ''
|
||||
: siteConfig.cloudPricingItems.find(item => item.name === 'Enterprise')?.stripePriceId || ''
|
||||
} />
|
||||
<SubmitButton
|
||||
pendingText="..."
|
||||
formAction={setupNewSubscription}
|
||||
className={cn(
|
||||
"w-full font-medium transition-colors h-7 rounded-md text-xs",
|
||||
tier.buttonColor
|
||||
)}
|
||||
>
|
||||
Upgrade
|
||||
</SubmitButton>
|
||||
</form>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Self-host Option as the third card */}
|
||||
<div className="border border-border rounded-lg p-3">
|
||||
<div className="text-center mb-2">
|
||||
<h3 className="font-medium">Self-Host</h3>
|
||||
<p className="text-sm font-bold">Free</p>
|
||||
<p className="text-xs text-muted-foreground">Open Source</p>
|
||||
</div>
|
||||
<Link
|
||||
href="https://github.com/kortix-ai/suna"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="w-full flex items-center justify-center gap-1 h-7 bg-gradient-to-tr from-primary to-primary/80 hover:opacity-90 text-white font-medium rounded-md text-xs transition-all"
|
||||
>
|
||||
<Github className="h-3.5 w-3.5" />
|
||||
<span>Self-Host</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
<X className="h-4 w-4" />
|
||||
<span className="sr-only">Close</span>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<AlertDialogHeader className="gap-6 px-8 pt-10 pb-6 relative z-10">
|
||||
<motion.div
|
||||
className="flex items-center justify-center"
|
||||
initial={{ y: -10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<div className="flex size-16 items-center justify-center rounded-full bg-gradient-to-t from-primary/20 to-secondary/10 backdrop-blur-md">
|
||||
<div className="flex size-12 items-center justify-center rounded-full bg-gradient-to-t from-primary to-primary/80 shadow-md">
|
||||
<Clock className="h-6 w-6 text-white" />
|
||||
</div>
|
||||
</motion.div>
|
||||
</div>
|
||||
</motion.div>
|
||||
</>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</Portal>
|
||||
|
||||
<motion.div
|
||||
initial={{ y: -10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<AlertDialogTitle className="text-2xl font-bold text-center text-primary bg-clip-text">
|
||||
High Demand Notice
|
||||
</AlertDialogTitle>
|
||||
</motion.div>
|
||||
|
||||
<motion.div
|
||||
initial={{ y: -10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.3 }}
|
||||
>
|
||||
<AlertDialogDescription className="text-base text-center leading-relaxed">
|
||||
Due to exceptionally high demand, our service is currently experiencing slower response times.
|
||||
We recommend returning tomorrow when our systems will be operating at normal capacity.
|
||||
<span className="mt-4 block font-medium text-primary">Thank you for your understanding. We will notify you via email once the service is fully operational again.</span>
|
||||
</AlertDialogDescription>
|
||||
</motion.div>
|
||||
</AlertDialogHeader>
|
||||
|
||||
<AlertDialogFooter className="p-8 pt-4 border-t border-border/40 bg-background/40 backdrop-blur-sm">
|
||||
<Link
|
||||
href="https://github.com/kortix-ai/suna"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mx-auto w-full flex items-center justify-center gap-3 bg-gradient-to-tr from-primary to-primary/80 hover:opacity-90 text-white font-medium rounded-full px-8 py-3 transition-all hover:shadow-md"
|
||||
>
|
||||
<Github className="h-5 w-5 transition-transform group-hover:scale-110" />
|
||||
<span>Explore Self-Hosted Version</span>
|
||||
</Link>
|
||||
</AlertDialogFooter>
|
||||
</motion.div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
)
|
||||
}
|
|
@ -273,12 +273,12 @@ export function NavUserWithTeams({
|
|||
Billing
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem asChild>
|
||||
{/* <DropdownMenuItem asChild>
|
||||
<Link href="/settings">
|
||||
<Settings className="mr-2 h-4 w-4" />
|
||||
Settings
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem onClick={() => setTheme(theme === "light" ? "dark" : "light")}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Sun className="mr-2 h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
import { useState, useCallback } from 'react';
|
||||
import { isLocalMode } from '@/lib/config';
|
||||
|
||||
interface BillingErrorState {
|
||||
message: string;
|
||||
|
@ -16,6 +17,12 @@ export function useBillingError() {
|
|||
const [billingError, setBillingError] = useState<BillingErrorState | null>(null);
|
||||
|
||||
const handleBillingError = useCallback((error: any) => {
|
||||
// In local mode, don't process billing errors
|
||||
if (isLocalMode()) {
|
||||
console.log('Running in local development mode - billing checks are disabled');
|
||||
return false;
|
||||
}
|
||||
|
||||
// Case 1: Error is already a formatted billing error detail object
|
||||
if (error && (error.message || error.subscription)) {
|
||||
setBillingError({
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
// Environment mode types
|
||||
export enum EnvMode {
|
||||
LOCAL = 'local',
|
||||
STAGING = 'staging',
|
||||
PRODUCTION = 'production',
|
||||
}
|
||||
|
||||
// Configuration object
|
||||
interface Config {
|
||||
ENV_MODE: EnvMode;
|
||||
IS_LOCAL: boolean;
|
||||
}
|
||||
|
||||
// Determine the environment mode from environment variables
|
||||
const getEnvironmentMode = (): EnvMode => {
|
||||
// Get the environment mode from the environment variable, if set
|
||||
const envMode = process.env.NEXT_PUBLIC_ENV_MODE?.toLowerCase();
|
||||
|
||||
// First check if the environment variable is explicitly set
|
||||
if (envMode) {
|
||||
if (envMode === EnvMode.LOCAL) {
|
||||
console.log('Using explicitly set LOCAL environment mode');
|
||||
return EnvMode.LOCAL;
|
||||
} else if (envMode === EnvMode.STAGING) {
|
||||
console.log('Using explicitly set STAGING environment mode');
|
||||
return EnvMode.STAGING;
|
||||
} else if (envMode === EnvMode.PRODUCTION) {
|
||||
console.log('Using explicitly set PRODUCTION environment mode');
|
||||
return EnvMode.PRODUCTION;
|
||||
}
|
||||
}
|
||||
|
||||
// If no valid environment mode is set, fall back to defaults based on NODE_ENV
|
||||
if (process.env.NODE_ENV === 'development') {
|
||||
console.log('Defaulting to LOCAL environment mode in development');
|
||||
return EnvMode.LOCAL;
|
||||
} else {
|
||||
console.log('Defaulting to PRODUCTION environment mode');
|
||||
return EnvMode.PRODUCTION;
|
||||
}
|
||||
};
|
||||
|
||||
// Get the environment mode once to ensure consistency
|
||||
const currentEnvMode = getEnvironmentMode();
|
||||
|
||||
// Create the config object
|
||||
export const config: Config = {
|
||||
ENV_MODE: currentEnvMode,
|
||||
IS_LOCAL: currentEnvMode === EnvMode.LOCAL,
|
||||
};
|
||||
|
||||
// Helper function to check if we're in local mode (for component conditionals)
|
||||
export const isLocalMode = (): boolean => {
|
||||
return config.IS_LOCAL;
|
||||
};
|
Loading…
Reference in New Issue