Merge pull request #748 from escapade-mckv/tolt

Tolt
This commit is contained in:
Bobbie 2025-06-16 17:59:59 +05:30 committed by GitHub
commit eddf6c2892
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 83 additions and 139 deletions

View File

@ -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

View File

@ -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()

View File

@ -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
)

View File

@ -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>
</>
);
}

View File

@ -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"

View File

@ -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));
},

View File

@ -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) {