mirror of https://github.com/kortix-ai/suna.git
commit
eddf6c2892
|
@ -115,7 +115,7 @@ python start.py
|
|||
|
||||
See the [Self-Hosting Guide](./docs/SELF-HOSTING.md) for detailed manual setup instructions.
|
||||
|
||||
The wizard will guide you through all necessary steps to get your Suna instance up and running. For detailed instructions, troubleshooting tips, and advanced configuration options, see the [Self-Hosting Guide](./SELF-HOSTING.md).
|
||||
The wizard will guide you through all necessary steps to get your Suna instance up and running. For detailed instructions, troubleshooting tips, and advanced configuration options, see the [Self-Hosting Guide](./docs/SELF-HOSTING.md).
|
||||
|
||||
## Contributing
|
||||
|
||||
|
|
|
@ -172,6 +172,10 @@ class ThreadManager:
|
|||
result: List[Dict[str, Any]] = []
|
||||
for msg in messages:
|
||||
msg_content = msg.get('content')
|
||||
# Try to parse msg_content as JSON if it's a string
|
||||
if isinstance(msg_content, str):
|
||||
try: msg_content = json.loads(msg_content)
|
||||
except json.JSONDecodeError: pass
|
||||
if isinstance(msg_content, dict):
|
||||
# Create a copy to avoid modifying the original
|
||||
msg_content_copy = msg_content.copy()
|
||||
|
|
|
@ -14,6 +14,8 @@ from services.supabase import DBConnection
|
|||
from utils.auth_utils import get_current_user_id_from_jwt
|
||||
from pydantic import BaseModel
|
||||
from utils.constants import MODEL_ACCESS_TIERS, MODEL_NAME_ALIASES
|
||||
import os
|
||||
|
||||
# Initialize Stripe
|
||||
stripe.api_key = config.STRIPE_SECRET_KEY
|
||||
|
||||
|
@ -37,6 +39,7 @@ class CreateCheckoutSessionRequest(BaseModel):
|
|||
price_id: str
|
||||
success_url: str
|
||||
cancel_url: str
|
||||
tolt_referral: Optional[str] = None
|
||||
|
||||
class CreatePortalSessionRequest(BaseModel):
|
||||
return_url: str
|
||||
|
@ -310,7 +313,7 @@ async def create_checkout_session(
|
|||
# Get or create Stripe customer
|
||||
customer_id = await get_stripe_customer_id(client, current_user_id)
|
||||
if not customer_id: customer_id = await create_stripe_customer(client, current_user_id, email)
|
||||
|
||||
|
||||
# Get the target price and product ID
|
||||
try:
|
||||
price = stripe.Price.retrieve(request.price_id, expand=['product'])
|
||||
|
@ -542,7 +545,7 @@ async def create_checkout_session(
|
|||
logger.exception(f"Error updating subscription {existing_subscription.get('id') if existing_subscription else 'N/A'}: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Error updating subscription: {str(e)}")
|
||||
else:
|
||||
# --- Create New Subscription via Checkout Session ---
|
||||
|
||||
session = stripe.checkout.Session.create(
|
||||
customer=customer_id,
|
||||
payment_method_types=['card'],
|
||||
|
@ -552,7 +555,8 @@ async def create_checkout_session(
|
|||
cancel_url=request.cancel_url,
|
||||
metadata={
|
||||
'user_id': current_user_id,
|
||||
'product_id': product_id
|
||||
'product_id': product_id,
|
||||
'tolt_referral': request.tolt_referral
|
||||
},
|
||||
allow_promotion_codes=True
|
||||
)
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { Separator } from '@/components/ui/separator';
|
||||
import Link from 'next/link';
|
||||
import { usePathname } from 'next/navigation';
|
||||
import Script from 'next/script';
|
||||
|
||||
export default function PersonalAccountSettingsPage({
|
||||
children,
|
||||
|
@ -16,30 +17,30 @@ export default function PersonalAccountSettingsPage({
|
|||
{ name: 'Billing', href: '/settings/billing' },
|
||||
];
|
||||
return (
|
||||
<div className="space-y-6 w-full">
|
||||
<Separator className="border-subtle dark:border-white/10" />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0 w-full max-w-6xl mx-auto px-4">
|
||||
<aside className="lg:w-1/4 p-1">
|
||||
<nav className="flex flex-col space-y-1">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${
|
||||
pathname === item.href
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'
|
||||
}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex-1 bg-card-bg dark:bg-background-secondary p-6 rounded-2xl border border-subtle dark:border-white/10 shadow-custom">
|
||||
{children}
|
||||
<>
|
||||
<div className="space-y-6 w-full">
|
||||
<Separator className="border-subtle dark:border-white/10" />
|
||||
<div className="flex flex-col space-y-8 lg:flex-row lg:space-x-12 lg:space-y-0 w-full max-w-6xl mx-auto px-4">
|
||||
<aside className="lg:w-1/4 p-1">
|
||||
<nav className="flex flex-col space-y-1">
|
||||
{items.map((item) => (
|
||||
<Link
|
||||
key={item.href}
|
||||
href={item.href}
|
||||
className={`px-3 py-2 rounded-md text-sm font-medium transition-colors ${pathname === item.href
|
||||
? 'bg-accent text-accent-foreground'
|
||||
: 'text-muted-foreground hover:bg-accent/50 hover:text-accent-foreground'}`}
|
||||
>
|
||||
{item.name}
|
||||
</Link>
|
||||
))}
|
||||
</nav>
|
||||
</aside>
|
||||
<div className="flex-1 bg-card-bg dark:bg-background-secondary p-6 rounded-2xl border border-subtle dark:border-white/10 shadow-custom">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -123,13 +123,12 @@ export default function RootLayout({
|
|||
'https://www.googletagmanager.com/gtm.js?id='+i+dl;f.parentNode.insertBefore(j,f);
|
||||
})(window,document,'script','dataLayer','GTM-PCHSN4M2');`}
|
||||
</Script>
|
||||
{/* End Google Tag Manager */}
|
||||
<Script async src="https://cdn.tolt.io/tolt.js" data-tolt={process.env.NEXT_PUBLIC_TOLT_REFERRAL_ID}></Script>
|
||||
</head>
|
||||
|
||||
<body
|
||||
className={`${geistSans.variable} ${geistMono.variable} antialiased font-sans bg-background`}
|
||||
>
|
||||
{/* Google Tag Manager (noscript) */}
|
||||
<noscript>
|
||||
<iframe
|
||||
src="https://www.googletagmanager.com/ns.html?id=GTM-PCHSN4M2"
|
||||
|
|
|
@ -70,6 +70,7 @@ export function useCachedFile<T = string>(
|
|||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<Error | null>(null);
|
||||
const { session } = useAuth();
|
||||
const [localBlobUrl, setLocalBlobUrl] = useState<string | null>(null);
|
||||
|
||||
// Calculate cache key from sandbox ID and file path
|
||||
const cacheKey = sandboxId && filePath
|
||||
|
@ -85,36 +86,9 @@ export function useCachedFile<T = string>(
|
|||
|
||||
if (!force && cached && now - cached.timestamp < expiration) {
|
||||
console.log(`[FILE CACHE] Returning cached content for ${key}`);
|
||||
|
||||
// Special handling for cached blobs - return a fresh URL
|
||||
if (FileCache.isImageFile(filePath || '') && cached.content instanceof Blob) {
|
||||
console.log(`[FILE CACHE] Creating fresh blob URL for cached image`);
|
||||
const blobUrl = URL.createObjectURL(cached.content);
|
||||
// Update the cache with the new blob URL to avoid creating multiple URLs for the same blob
|
||||
console.log(`[FILE CACHE] Updating cache with fresh blob URL: ${blobUrl}`);
|
||||
fileCache.set(key, {
|
||||
content: blobUrl,
|
||||
timestamp: now,
|
||||
type: 'url'
|
||||
});
|
||||
return blobUrl;
|
||||
} else if (cached.type === 'url' && typeof cached.content === 'string' && cached.content.startsWith('blob:')) {
|
||||
// For blob URLs, verify they're still valid
|
||||
try {
|
||||
// This is a simple check that won't actually fetch the blob, just verify the URL is valid
|
||||
const xhr = new XMLHttpRequest();
|
||||
xhr.open('HEAD', cached.content, false);
|
||||
xhr.send();
|
||||
return cached.content;
|
||||
} catch (err) {
|
||||
console.warn(`[FILE CACHE] Cached blob URL is invalid, will refetch: ${err}`);
|
||||
// Force a refetch
|
||||
force = true;
|
||||
}
|
||||
}
|
||||
|
||||
return cached.content;
|
||||
}
|
||||
|
||||
console.log(`[FILE CACHE] Fetching fresh content for ${key}`);
|
||||
// Fetch fresh content if no cache or expired
|
||||
setIsLoading(true);
|
||||
|
@ -192,60 +166,25 @@ export function useCachedFile<T = string>(
|
|||
if (isPdfFile && !blob.type.includes('pdf') && blob.size > 0) {
|
||||
console.warn(`[FILE CACHE] PDF blob has generic MIME type: ${blob.type} - will correct it automatically`);
|
||||
|
||||
// Check if the content looks like a PDF
|
||||
const firstBytes = await blob.slice(0, 10).text();
|
||||
if (firstBytes.startsWith('%PDF')) {
|
||||
console.log(`[FILE CACHE] Content appears to be a PDF despite incorrect MIME type, proceeding`);
|
||||
|
||||
// Create a new blob with the correct type
|
||||
const correctedBlob = new Blob([await blob.arrayBuffer()], { type: 'application/pdf' });
|
||||
console.log(`[FILE CACHE] Created corrected PDF blob with proper MIME type (${correctedBlob.size} bytes)`);
|
||||
|
||||
// Store the corrected blob in cache
|
||||
const specificKey = `${sandboxId}:${normalizePath(filePath)}:blob`;
|
||||
fileCache.set(specificKey, {
|
||||
content: correctedBlob,
|
||||
timestamp: Date.now(),
|
||||
type: 'content'
|
||||
});
|
||||
|
||||
// Also update the general key
|
||||
fileCache.set(key, {
|
||||
content: correctedBlob,
|
||||
timestamp: Date.now(),
|
||||
type: 'content'
|
||||
});
|
||||
|
||||
// Return a URL for immediate use
|
||||
const blobUrl = URL.createObjectURL(correctedBlob);
|
||||
console.log(`[FILE CACHE] Created fresh blob URL for corrected PDF: ${blobUrl}`);
|
||||
return blobUrl;
|
||||
// Store the corrected blob in cache and return it
|
||||
fileCache.set(key, { content: correctedBlob, timestamp: Date.now(), type: 'content' });
|
||||
return correctedBlob;
|
||||
}
|
||||
}
|
||||
|
||||
// Store the raw blob in cache - using a more specific key that includes content type
|
||||
const specificKey = `${sandboxId}:${normalizePath(filePath)}:blob`;
|
||||
fileCache.set(specificKey, {
|
||||
content: blob,
|
||||
timestamp: Date.now(),
|
||||
type: 'content'
|
||||
});
|
||||
|
||||
// Also update the general key
|
||||
fileCache.set(key, {
|
||||
content: blob,
|
||||
timestamp: Date.now(),
|
||||
type: 'content'
|
||||
});
|
||||
|
||||
// But return a URL for immediate use
|
||||
const blobUrl = URL.createObjectURL(blob);
|
||||
console.log(`[FILE CACHE] Created fresh blob URL for immediate use: ${blobUrl}`);
|
||||
return blobUrl; // Return early since we've already cached
|
||||
// Store the raw blob in cache and return it
|
||||
fileCache.set(key, { content: blob, timestamp: Date.now(), type: 'content' });
|
||||
return blob;
|
||||
} else {
|
||||
// For other binary files, create and return a blob URL
|
||||
content = URL.createObjectURL(blob);
|
||||
cacheType = 'url';
|
||||
// For other binary files, content is the blob
|
||||
content = blob;
|
||||
cacheType = 'content';
|
||||
}
|
||||
break;
|
||||
case 'arrayBuffer':
|
||||
|
@ -263,14 +202,12 @@ export function useCachedFile<T = string>(
|
|||
break;
|
||||
}
|
||||
|
||||
// Only cache if we haven't already cached (for images and PDFs)
|
||||
if (!isImageFile && !isPdfFile) {
|
||||
fileCache.set(key, {
|
||||
content,
|
||||
timestamp: now,
|
||||
type: cacheType
|
||||
});
|
||||
}
|
||||
// After the switch, the caching logic should be simplified to handle all cases that fall through
|
||||
fileCache.set(key, {
|
||||
content,
|
||||
timestamp: now,
|
||||
type: cacheType
|
||||
});
|
||||
|
||||
return content;
|
||||
} catch (err: any) {
|
||||
|
@ -287,41 +224,43 @@ export function useCachedFile<T = string>(
|
|||
}
|
||||
};
|
||||
|
||||
// Function to force refresh the cache
|
||||
const refreshCache = async () => {
|
||||
if (!cacheKey) return null;
|
||||
try {
|
||||
const freshData = await getCachedFile(cacheKey, true);
|
||||
setData(freshData);
|
||||
return freshData;
|
||||
} catch (err: any) {
|
||||
setError(err);
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Function to get data from cache first, then network if needed
|
||||
const getFileContent = async () => {
|
||||
if (!cacheKey) return;
|
||||
|
||||
const processContent = (content: any) => {
|
||||
if (localBlobUrl) {
|
||||
URL.revokeObjectURL(localBlobUrl);
|
||||
}
|
||||
|
||||
if (content instanceof Blob) {
|
||||
const newUrl = URL.createObjectURL(content);
|
||||
setLocalBlobUrl(newUrl);
|
||||
setData(newUrl as any);
|
||||
} else {
|
||||
setLocalBlobUrl(null);
|
||||
setData(content);
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
// First check if we have cached data
|
||||
const cachedItem = fileCache.get(cacheKey);
|
||||
if (cachedItem) {
|
||||
// Set data from cache immediately
|
||||
setData(cachedItem.content);
|
||||
processContent(cachedItem.content);
|
||||
|
||||
// If cache is expired, refresh in background
|
||||
if (Date.now() - cachedItem.timestamp > (options.expiration || CACHE_EXPIRATION)) {
|
||||
getCachedFile(cacheKey, true)
|
||||
.then(freshData => setData(freshData))
|
||||
.then(freshData => processContent(freshData))
|
||||
.catch(err => console.error("Background refresh failed:", err));
|
||||
}
|
||||
} else {
|
||||
// No cache, load fresh
|
||||
setIsLoading(true);
|
||||
const content = await getCachedFile(cacheKey);
|
||||
setData(content);
|
||||
processContent(content);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err);
|
||||
|
@ -340,18 +279,11 @@ export function useCachedFile<T = string>(
|
|||
setError(null);
|
||||
}
|
||||
|
||||
// Clean up any blob URLs when component unmounts
|
||||
// Clean up the local blob URL when component unmounts
|
||||
return () => {
|
||||
if (cacheKey) {
|
||||
const cachedData = fileCache.get(cacheKey);
|
||||
if (cachedData?.type === 'url') {
|
||||
// Only revoke if it's a URL type (created URL instead of raw blob)
|
||||
const cachedUrl = cachedData.content;
|
||||
if (typeof cachedUrl === 'string' && cachedUrl.startsWith('blob:')) {
|
||||
console.log(`[FILE CACHE][IMAGE DEBUG] Cleaning up blob URL on unmount: ${cachedUrl} for key ${cacheKey}`);
|
||||
URL.revokeObjectURL(cachedUrl);
|
||||
}
|
||||
}
|
||||
if (localBlobUrl) {
|
||||
URL.revokeObjectURL(localBlobUrl);
|
||||
setLocalBlobUrl(null);
|
||||
}
|
||||
};
|
||||
}, [sandboxId, filePath, options.contentType]);
|
||||
|
@ -361,7 +293,6 @@ export function useCachedFile<T = string>(
|
|||
data,
|
||||
isLoading,
|
||||
error,
|
||||
refreshCache,
|
||||
getCachedFile: (key?: string, force = false) => {
|
||||
return key ? getCachedFile(key, force) : (cacheKey ? getCachedFile(cacheKey, force) : Promise.resolve(null));
|
||||
},
|
||||
|
|
|
@ -1500,6 +1500,7 @@ export interface CreateCheckoutSessionRequest {
|
|||
price_id: string;
|
||||
success_url: string;
|
||||
cancel_url: string;
|
||||
referral_id?: string;
|
||||
}
|
||||
|
||||
export interface CreatePortalSessionRequest {
|
||||
|
@ -1588,14 +1589,18 @@ export const createCheckoutSession = async (
|
|||
if (!session?.access_token) {
|
||||
throw new NoAccessTokenAvailableError();
|
||||
}
|
||||
|
||||
|
||||
|
||||
const requestBody = { ...request, tolt_referral: window.tolt_referral };
|
||||
console.log('Tolt Referral ID:', requestBody.tolt_referral);
|
||||
|
||||
const response = await fetch(`${API_URL}/billing/create-checkout-session`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: `Bearer ${session.access_token}`,
|
||||
},
|
||||
body: JSON.stringify(request),
|
||||
body: JSON.stringify(requestBody),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
|
Loading…
Reference in New Issue