Compare commits

...

5 Commits

Author SHA1 Message Date
Bobbie 967b68feb9
Merge pull request #1438 from escapade-mckv/bug-fixes-5
merge kortix agents with community
2025-08-24 11:27:33 +05:30
Saumya 14a86b753b merge kortix agents with community 2025-08-24 11:26:12 +05:30
marko-kraemer ebdddbb580 fix 2025-08-23 21:39:55 -07:00
marko-kraemer e5d4223d4d wip 2025-08-23 21:30:42 -07:00
marko-kraemer 0b0efdf5eb wip 2025-08-23 21:16:27 -07:00
10 changed files with 416 additions and 184 deletions

View File

@ -116,6 +116,110 @@ Use the `presentation_styles` tool to show available options:
- **Style Consistency**: All slides in a presentation should use the same style
- **Color Classes**: Use `.primary-color`, `.accent-color`, `.primary-bg`, `.accent-bg`, `.text-color`
### 🚨 CRITICAL BOUNDARY & CONTAINMENT RULES
**ABSOLUTE REQUIREMENT**: **MAKE SURE EVERYTHING STAYS WITHIN BOUNDS AND NOTHING GOES OUT**
#### **1. Slide Boundary Enforcement**
- **Fixed Container**: Every slide MUST use `height: 100vh; width: 100vw; overflow: hidden;` on the root container
- **No Overflow**: NEVER allow any element to extend beyond 1920x1080 boundaries
- **Safe Margins**: Always maintain minimum 40px margins from all edges for critical content
- **Edge Protection**: Keep important text/elements at least 60px from slide edges
#### **2. Content Containment Rules**
- **Text Wrapping**: All text MUST wrap properly within containers using `word-wrap: break-word;`
- **Image Sizing**: Images MUST use `max-width: 100%; height: auto;` and proper container constraints
- **Absolute Positioning**: When using `position: absolute`, always set explicit boundaries with `max-width` and `max-height`
- **Flex/Grid Overflow**: Use `min-width: 0` on flex/grid items to prevent overflow
#### **3. CSS Containment Requirements**
```css
/* MANDATORY root container styles */
.slide-container {
width: 1920px;
height: 1080px;
overflow: hidden;
position: relative;
box-sizing: border-box;
}
/* REQUIRED for all content containers */
.content-container {
max-width: 100%;
max-height: 100%;
overflow: hidden;
box-sizing: border-box;
}
```
#### **4. Element-Specific Boundary Rules**
- **Long Titles**: Use `font-size: clamp()` or responsive sizing to prevent title overflow
- **Lists**: Limit list items and use `overflow-y: auto` with max-height if needed
- **Tables**: Always use `table-layout: fixed; width: 100%;` with column width constraints
- **Charts/Graphics**: Set explicit `width` and `height` with `max-width: 100%`
- **Background Images**: Use `background-size: cover` or `contain` appropriately, never `background-size: auto`
#### **5. Animation & Transform Boundaries**
- **CSS Animations**: Ensure all keyframes keep elements within slide bounds
- **Transforms**: Use `transform-origin` carefully and test all transform states
- **Floating Elements**: Animated floating elements MUST have boundary constraints
- **Hover Effects**: Hover states cannot cause elements to exceed slide dimensions
#### **6. Responsive Containment**
- **Viewport Units**: Use `vw/vh` cautiously, prefer `%` within containers
- **Media Queries**: Include breakpoints to handle edge cases in different viewing contexts
- **Scaling**: When scaling content, maintain aspect ratios and boundary compliance
- **Dynamic Content**: Test with varying content lengths to ensure no overflow
#### **7. Testing & Validation Requirements**
- **Boundary Testing**: Mentally verify every element stays within 1920x1080 bounds
- **Content Stress Testing**: Test with maximum expected content length
- **Edge Case Validation**: Check corners, edges, and extreme content scenarios
- **Cross-browser Consistency**: Ensure containment works across different rendering engines
#### **8. Emergency Containment Techniques**
```css
/* Use when content might overflow */
.overflow-protection {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap; /* for single line */
}
/* For multi-line text containment */
.multiline-containment {
display: -webkit-box;
-webkit-line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
}
/* For absolute positioned elements */
.absolute-contained {
position: absolute;
max-width: calc(100% - 80px); /* account for margins */
max-height: calc(100% - 80px);
overflow: hidden;
}
```
#### **9. Content Priority Rules**
- **Critical Content First**: Most important content gets priority positioning within safe zones
- **Progressive Enhancement**: Less critical content can be hidden/truncated if space is limited
- **Hierarchy Preservation**: Maintain visual hierarchy even when constraining content
- **Readability Over Quantity**: Better to have less content that's fully visible than overflow
#### **10. Quality Assurance Checklist**
Before finalizing any slide, verify:
- ✅ All text is fully visible and readable
- ✅ All images are completely within bounds
- ✅ No horizontal or vertical scrollbars appear
- ✅ Animations stay within slide boundaries
- ✅ Content adapts gracefully to container constraints
- ✅ No elements are cut off at any edge
- ✅ Safe margins are maintained around critical content
- ✅ Responsive behavior maintains containment
## Image Integration & Visual Content
### Image Sources & Integration

View File

@ -414,7 +414,7 @@ async def calculate_monthly_usage(client, user_id: str) -> float:
async def get_usage_logs(client, user_id: str, page: int = 0, items_per_page: int = 1000) -> Dict:
"""Get detailed usage logs for a user with pagination, including credit usage info."""
logger.info(f"[USAGE_LOGS] Starting get_usage_logs for user_id={user_id}, page={page}, items_per_page={items_per_page}")
logger.debug(f"[USAGE_LOGS] Starting get_usage_logs for user_id={user_id}, page={page}, items_per_page={items_per_page}")
try:
# Get start of current month in UTC
@ -458,10 +458,10 @@ async def get_usage_logs(client, user_id: str, page: int = 0, items_per_page: in
logger.error(f"[USAGE_LOGS] user_id={user_id} - Error fetching threads batch at offset {offset}: {str(thread_error)}")
raise
logger.info(f"[USAGE_LOGS] user_id={user_id} - Found {len(all_threads)} total threads")
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Found {len(all_threads)} total threads")
if not all_threads:
logger.info(f"[USAGE_LOGS] user_id={user_id} - No threads found, returning empty result")
logger.debug(f"[USAGE_LOGS] user_id={user_id} - No threads found, returning empty result")
return {"logs": [], "has_more": False}
thread_ids = [t['thread_id'] for t in all_threads]
@ -538,10 +538,10 @@ async def get_usage_logs(client, user_id: str, page: int = 0, items_per_page: in
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Database query for usage logs took {execution_time:.3f} seconds")
if not messages_result.data:
logger.info(f"[USAGE_LOGS] user_id={user_id} - No messages found, returning empty result")
logger.debug(f"[USAGE_LOGS] user_id={user_id} - No messages found, returning empty result")
return {"logs": [], "has_more": False}
logger.info(f"[USAGE_LOGS] user_id={user_id} - Found {len(messages_result.data)} messages to process")
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Found {len(messages_result.data)} messages to process")
# Get the user's subscription tier info for credit checking
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Getting subscription info")
@ -691,7 +691,7 @@ async def get_usage_logs(client, user_id: str, page: int = 0, items_per_page: in
logger.error(f"[USAGE_LOGS] user_id={user_id} - Error processing usage log entry for message {message.get('message_id', 'unknown')}: {str(e)}")
continue
logger.info(f"[USAGE_LOGS] user_id={user_id} - Successfully processed {len(processed_logs)} messages")
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Successfully processed {len(processed_logs)} messages")
# Check if there are more results
has_more = len(processed_logs) == items_per_page
@ -719,7 +719,7 @@ async def get_usage_logs(client, user_id: str, page: int = 0, items_per_page: in
"error": "Failed to serialize usage data"
}
logger.info(f"[USAGE_LOGS] user_id={user_id} - Returning {len(processed_logs)} logs, has_more={has_more}")
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Returning {len(processed_logs)} logs, has_more={has_more}")
return result
except Exception as outer_error:
@ -1035,7 +1035,7 @@ async def is_user_on_highest_tier(user_id: str) -> bool:
if subscription.get('items') and subscription['items'].get('data') and len(subscription['items']['data']) > 0:
price_id = subscription['items']['data'][0]['price']['id']
logger.info(f"User {user_id} subscription price_id: {price_id}")
logger.debug(f"User {user_id} subscription price_id: {price_id}")
# Check if it's one of the highest tier price IDs (200h/$1000 only)
highest_tier_price_ids = [
@ -1048,7 +1048,7 @@ async def is_user_on_highest_tier(user_id: str) -> bool:
]
is_highest = price_id in highest_tier_price_ids
logger.info(f"User {user_id} is_highest_tier: {is_highest}, price_id: {price_id}, checked against: {highest_tier_price_ids}")
logger.debug(f"User {user_id} is_highest_tier: {is_highest}, price_id: {price_id}, checked against: {highest_tier_price_ids}")
return is_highest
@ -1977,7 +1977,7 @@ async def stripe_webhook(request: Request):
purchase_id = purchase_update.data[0]['id'] if purchase_update.data else None
new_balance = await add_credits_to_balance(client, user_id, credit_amount, purchase_id)
logger.info(f"Successfully added ${credit_amount} credits to user {user_id}. New balance: ${new_balance}")
logger.debug(f"Successfully added ${credit_amount} credits to user {user_id}. New balance: ${new_balance}")
# Clear cache for this user
await Cache.delete(f"monthly_usage:{user_id}")
@ -2198,7 +2198,7 @@ async def get_usage_logs_endpoint(
current_user_id: str = Depends(get_current_user_id_from_jwt)
):
"""Get detailed usage logs for a user with pagination."""
logger.info(f"[USAGE_LOGS_ENDPOINT] Starting get_usage_logs_endpoint for user_id={current_user_id}, page={page}, items_per_page={items_per_page}")
logger.debug(f"[USAGE_LOGS_ENDPOINT] Starting get_usage_logs_endpoint for user_id={current_user_id}, page={page}, items_per_page={items_per_page}")
try:
# Get Supabase client
@ -2231,7 +2231,7 @@ async def get_usage_logs_endpoint(
logger.error(f"[USAGE_LOGS_ENDPOINT] user_id={current_user_id} - Usage logs returned error: {result['error']}")
raise HTTPException(status_code=400, detail=f"Failed to retrieve usage logs: {result['error']}")
logger.info(f"[USAGE_LOGS_ENDPOINT] user_id={current_user_id} - Successfully returned {len(result.get('logs', []))} usage logs")
logger.debug(f"[USAGE_LOGS_ENDPOINT] user_id={current_user_id} - Successfully returned {len(result.get('logs', []))} usage logs")
return result
except HTTPException:

View File

@ -119,12 +119,22 @@ export default function AgentsPage() {
return params;
}, [agentsPage, agentsSearchQuery, agentsSortBy, agentsSortOrder, agentsFilters]);
const marketplaceQueryParams = useMemo(() => ({
limit: 20,
offset: (marketplacePage - 1) * 20,
search: marketplaceSearchQuery || undefined,
tags: marketplaceSelectedTags.length > 0 ? marketplaceSelectedTags.join(',') : undefined,
}), [marketplacePage, marketplaceSearchQuery, marketplaceSelectedTags]);
const marketplaceQueryParams = useMemo(() => {
const params: any = {
limit: 20,
offset: (marketplacePage - 1) * 20,
search: marketplaceSearchQuery || undefined,
tags: marketplaceSelectedTags.length > 0 ? marketplaceSelectedTags.join(',') : undefined,
};
if (marketplaceFilter === 'kortix') {
params.is_kortix_team = true;
} else if (marketplaceFilter === 'community') {
params.is_kortix_team = false;
}
return params;
}, [marketplacePage, marketplaceSearchQuery, marketplaceSelectedTags, marketplaceFilter]);
const { data: agentsResponse, isLoading: agentsLoading, error: agentsError, refetch: loadAgents } = useAgents(agentsQueryParams);
const { data: marketplaceTemplates, isLoading: marketplaceLoading } = useMarketplaceTemplates(marketplaceQueryParams);
@ -144,9 +154,8 @@ export default function AgentsPage() {
const agents = agentsResponse?.agents || [];
const agentsPagination = agentsResponse?.pagination;
const { kortixTeamItems, communityItems, mineItems } = useMemo(() => {
const kortixItems: MarketplaceTemplate[] = [];
const communityItems: MarketplaceTemplate[] = [];
const { allMarketplaceItems, mineItems } = useMemo(() => {
const allItems: MarketplaceTemplate[] = [];
const mineItems: MarketplaceTemplate[] = [];
if (marketplaceTemplates) {
@ -168,61 +177,20 @@ export default function AgentsPage() {
metadata: template.metadata,
};
const matchesSearch = !marketplaceSearchQuery.trim() || (() => {
const searchLower = marketplaceSearchQuery.toLowerCase();
return item.name.toLowerCase().includes(searchLower) ||
item.description?.toLowerCase().includes(searchLower) ||
item.tags.some(tag => tag.toLowerCase().includes(searchLower)) ||
item.creator_name?.toLowerCase().includes(searchLower);
})();
if (!matchesSearch) return;
// Backend handles search filtering, so we just transform the data
allItems.push(item);
if (user?.id === template.creator_id) {
mineItems.push(item);
}
if (template.is_kortix_team === true) {
kortixItems.push(item);
} else {
communityItems.push(item);
}
});
}
const sortItems = (items: MarketplaceTemplate[]) =>
items.sort((a, b) => {
switch (marketplaceSortBy) {
case 'newest':
return new Date(b.marketplace_published_at || b.created_at).getTime() -
new Date(a.marketplace_published_at || a.created_at).getTime();
case 'popular':
case 'most_downloaded':
return b.download_count - a.download_count;
case 'name':
return a.name.localeCompare(b.name);
default:
return 0;
}
});
return {
kortixTeamItems: sortItems(kortixItems),
communityItems: sortItems(communityItems),
mineItems: sortItems(mineItems)
allMarketplaceItems: allItems,
mineItems: mineItems
};
}, [marketplaceTemplates, marketplaceSortBy, user?.id, marketplaceSearchQuery]);
const allMarketplaceItems = useMemo(() => {
if (marketplaceFilter === 'kortix') {
return kortixTeamItems;
} else if (marketplaceFilter === 'community') {
return communityItems;
} else if (marketplaceFilter === 'mine') {
return mineItems;
}
return [...kortixTeamItems, ...communityItems];
}, [kortixTeamItems, communityItems, mineItems, marketplaceFilter]);
}, [marketplaceTemplates, user?.id]);
const handleTabChange = (newTab: string) => {
const params = new URLSearchParams(searchParams.toString());
@ -611,9 +579,7 @@ export default function AgentsPage() {
marketplaceFilter={marketplaceFilter}
setMarketplaceFilter={setMarketplaceFilter}
marketplaceLoading={marketplaceLoading}
allMarketplaceItems={allMarketplaceItems}
kortixTeamItems={kortixTeamItems}
communityItems={communityItems}
allMarketplaceItems={marketplaceFilter === 'mine' ? mineItems : allMarketplaceItems}
mineItems={mineItems}
installingItemId={installingItemId}
onInstallClick={handleInstallClick}

View File

@ -701,7 +701,7 @@ export default function ThreadPage({
className={cn(
"fixed bottom-0 z-10 bg-gradient-to-t from-background via-background/90 to-transparent px-4 pt-8",
isSidePanelAnimating ? "" : "transition-all duration-200 ease-in-out",
leftSidebarState === 'expanded' ? 'left-[72px] md:left-[256px]' : 'left-[72px]',
leftSidebarState === 'expanded' ? 'left-[72px] md:left-[256px]' : 'left-[40px]',
isSidePanelOpen && !isMobile ? 'right-[90%] sm:right-[450px] md:right-[500px] lg:right-[550px] xl:right-[650px]' : 'right-0',
isMobile ? 'left-0 right-0' : ''
)}>

View File

@ -100,13 +100,8 @@ export function AgentModelSelector({
const normalizedValue = normalizeModelId(value);
useEffect(() => {
if (normalizedValue && normalizedValue !== storeSelectedModel) {
storeHandleModelChange(normalizedValue);
}
}, [normalizedValue, storeSelectedModel, storeHandleModelChange]);
const selectedModel = storeSelectedModel;
// Use the prop value if provided, otherwise fall back to store value
const selectedModel = normalizedValue || storeSelectedModel;
const enhancedModelOptions = useMemo(() => {
const modelMap = new Map();
@ -202,7 +197,6 @@ export function AgentModelSelector({
const isCustomModel = customModels.some(model => model.id === modelId);
if (isCustomModel && isLocalMode()) {
storeHandleModelChange(modelId);
onChange(modelId);
setIsOpen(false);
return;
@ -210,7 +204,6 @@ export function AgentModelSelector({
const hasAccess = isLocalMode() || canAccessModel(modelId);
if (hasAccess) {
storeHandleModelChange(modelId);
onChange(modelId);
setIsOpen(false);
} else {
@ -285,12 +278,10 @@ export function AgentModelSelector({
if (dialogMode === 'add') {
storeAddCustomModel(newModel);
storeHandleModelChange(modelId);
onChange(modelId);
} else {
storeUpdateCustomModel(editingModelId!, newModel);
if (selectedModel === editingModelId) {
storeHandleModelChange(modelId);
onChange(modelId);
}
}
@ -312,7 +303,6 @@ export function AgentModelSelector({
if (selectedModel === modelId) {
const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
storeHandleModelChange(defaultModel);
onChange(defaultModel);
}
};
@ -327,9 +317,8 @@ export function AgentModelSelector({
const isRecommended = MODELS[model.id]?.recommended || false;
return (
<TooltipProvider key={`model-${model.id}-${index}`}>
<Tooltip>
<TooltipTrigger asChild>
<Tooltip key={`model-${model.id}-${index}`}>
<TooltipTrigger asChild>
<div className='w-full'>
<DropdownMenuItem
className={cn(
@ -402,16 +391,14 @@ export function AgentModelSelector({
</TooltipContent>
) : null}
</Tooltip>
</TooltipProvider>
);
};
return (
<div className="relative">
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<DropdownMenuTrigger asChild disabled={disabled}>
{variant === 'menu-item' ? (
<div
@ -463,8 +450,7 @@ export function AgentModelSelector({
<TooltipContent side={variant === 'menu-item' ? 'left' : 'top'} className="text-xs">
<p>Choose a model for this agent</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Tooltip>
<DropdownMenuContent
align={variant === 'menu-item' ? 'end' : 'start'}
className="w-76 p-0 overflow-hidden"
@ -476,9 +462,8 @@ export function AgentModelSelector({
<span className="text-xs font-medium text-muted-foreground p-2 px-4">All Models</span>
{isLocalMode() && (
<div className="flex items-center gap-1 p-2">
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Tooltip>
<TooltipTrigger asChild>
<Link
href="/settings/env-manager"
className="h-6 w-6 p-0 flex items-center justify-center"
@ -489,11 +474,9 @@ export function AgentModelSelector({
<TooltipContent side="bottom" className="text-xs">
Local .Env Manager
</TooltipContent>
</Tooltip>
</TooltipProvider>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button
size="sm"
variant="ghost"
@ -509,8 +492,7 @@ export function AgentModelSelector({
<TooltipContent side="bottom" className="text-xs">
Add a custom model
</TooltipContent>
</Tooltip>
</TooltipProvider>
</Tooltip>
</div>
)}
</div>
@ -549,9 +531,8 @@ export function AgentModelSelector({
const isRecommended = model.recommended;
return (
<TooltipProvider key={`premium-${model.id}-${index}`}>
<Tooltip>
<TooltipTrigger asChild>
<Tooltip key={`premium-${model.id}-${index}`}>
<TooltipTrigger asChild>
<div className='w-full'>
<DropdownMenuItem
className={cn(
@ -586,7 +567,6 @@ export function AgentModelSelector({
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
);
})}
{subscriptionStatus !== 'active' && (

View File

@ -16,8 +16,6 @@ interface MarketplaceTabProps {
setMarketplaceFilter: (value: 'all' | 'kortix' | 'community' | 'mine') => void;
marketplaceLoading: boolean;
allMarketplaceItems: MarketplaceTemplate[];
kortixTeamItems: MarketplaceTemplate[];
communityItems: MarketplaceTemplate[];
mineItems: MarketplaceTemplate[];
installingItemId: string | null;
onInstallClick: (item: MarketplaceTemplate, e?: React.MouseEvent) => void;
@ -34,8 +32,6 @@ export const MarketplaceTab = ({
setMarketplaceFilter,
marketplaceLoading,
allMarketplaceItems,
kortixTeamItems,
communityItems,
mineItems,
installingItemId,
onInstallClick,
@ -101,55 +97,27 @@ export const MarketplaceTab = ({
) : (
<div className="space-y-12">
{marketplaceFilter === 'all' ? (
<>
{kortixTeamItems.length > 0 && (
<div className="space-y-6">
<MarketplaceSectionHeader
title="By team Kortix"
subtitle="Official agents, maintained and supported"
<div className="space-y-6">
{/* <MarketplaceSectionHeader
title="Popular Agents"
subtitle="Sorted by popularity - most downloads first"
/> */}
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{allMarketplaceItems.map((item) => (
<AgentCard
key={item.id}
mode="marketplace"
data={item}
styling={getItemStyling(item)}
isActioning={installingItemId === item.id}
onPrimaryAction={onInstallClick}
onDeleteAction={onDeleteTemplate}
onClick={() => handleAgentClick(item)}
currentUserId={currentUserId}
/>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{kortixTeamItems.map((item) => (
<AgentCard
key={item.id}
mode="marketplace"
data={item}
styling={getItemStyling(item)}
isActioning={installingItemId === item.id}
onPrimaryAction={onInstallClick}
onDeleteAction={onDeleteTemplate}
onClick={() => handleAgentClick(item)}
currentUserId={currentUserId}
/>
))}
</div>
</div>
)}
{communityItems.length > 0 && (
<div className="space-y-6">
<MarketplaceSectionHeader
title="From the community"
subtitle="Agents created by our community"
iconColor="bg-gradient-to-br from-green-500 to-green-600"
/>
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{communityItems.map((item) => (
<AgentCard
key={item.id}
mode="marketplace"
data={item}
styling={getItemStyling(item)}
isActioning={installingItemId === item.id}
onPrimaryAction={onInstallClick}
onDeleteAction={onDeleteTemplate}
onClick={() => handleAgentClick(item)}
currentUserId={currentUserId}
/>
))}
</div>
</div>
)}
</>
))}
</div>
</div>
) : (
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{allMarketplaceItems.map((item) => (

View File

@ -60,6 +60,8 @@ export function FullScreenPresentationViewer({
const [currentSlide, setCurrentSlide] = useState(initialSlide);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [retryAttempt, setRetryAttempt] = useState(0);
const [backgroundRetryInterval, setBackgroundRetryInterval] = useState<NodeJS.Timeout | null>(null);
const [showControls, setShowControls] = useState(true);
const [showEditor, setShowEditor] = useState(false);
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
@ -78,12 +80,13 @@ export function FullScreenPresentationViewer({
return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase();
};
// Load metadata
const loadMetadata = useCallback(async () => {
// Load metadata with retry logic
const loadMetadata = useCallback(async (retryCount = 0, maxRetries = 5) => {
if (!presentationName || !sandboxUrl) return;
setIsLoading(true);
setError(null);
setRetryAttempt(retryCount);
try {
// Sanitize the presentation name to match backend directory creation
@ -94,7 +97,10 @@ export function FullScreenPresentationViewer({
`presentations/${sanitizedPresentationName}/metadata.json`
);
const response = await fetch(`${metadataUrl}?t=${Date.now()}`, {
const urlWithCacheBust = `${metadataUrl}?t=${Date.now()}`;
console.log(`Loading presentation metadata (attempt ${retryCount + 1}/${maxRetries + 1}):`, urlWithCacheBust);
const response = await fetch(urlWithCacheBust, {
cache: 'no-cache',
headers: { 'Cache-Control': 'no-cache' }
});
@ -102,23 +108,75 @@ export function FullScreenPresentationViewer({
if (response.ok) {
const data = await response.json();
setMetadata(data);
console.log('Successfully loaded presentation metadata:', data);
setIsLoading(false);
// Clear background retry interval on success
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
setBackgroundRetryInterval(null);
}
return; // Success, exit early
} else {
setError('Failed to load presentation metadata');
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (err) {
console.error('Error loading metadata:', err);
setError('Failed to load presentation metadata');
} finally {
console.error(`Error loading metadata (attempt ${retryCount + 1}):`, err);
// If we haven't reached max retries, try again with exponential backoff
if (retryCount < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Cap at 10 seconds
console.log(`Retrying in ${delay}ms...`);
setTimeout(() => {
loadMetadata(retryCount + 1, maxRetries);
}, delay);
return; // Don't set error state yet, we're retrying
}
// All retries exhausted, set error and start background retry
setError('Failed to load presentation metadata after multiple attempts');
setIsLoading(false);
// Start background retry every 10 seconds
if (!backgroundRetryInterval) {
const interval = setInterval(() => {
console.log('Background retry attempt...');
loadMetadata(0, 2); // Fewer retries for background attempts
}, 10000);
setBackgroundRetryInterval(interval);
}
}
}, [presentationName, sandboxUrl]);
}, [presentationName, sandboxUrl, backgroundRetryInterval]);
useEffect(() => {
if (isOpen) {
// Clear any existing background retry when opening
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
setBackgroundRetryInterval(null);
}
loadMetadata();
setCurrentSlide(initialSlide);
} else {
// Clear background retry when closing
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
setBackgroundRetryInterval(null);
}
}
}, [isOpen, loadMetadata, initialSlide]);
}, [isOpen, loadMetadata, initialSlide, backgroundRetryInterval]);
// Cleanup background retry interval on unmount
useEffect(() => {
return () => {
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
}
};
}, [backgroundRetryInterval]);
// Reload metadata when exiting editor mode to refresh with latest changes
useEffect(() => {
@ -402,13 +460,37 @@ export function FullScreenPresentationViewer({
{isLoading ? (
<div className="text-center">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-zinc-600 mx-auto mb-4"></div>
<p className="text-zinc-700 dark:text-zinc-300">Loading presentation...</p>
<p className="text-zinc-700 dark:text-zinc-300">
{retryAttempt > 0 ? `Retrying... (attempt ${retryAttempt + 1})` : 'Loading presentation...'}
</p>
</div>
) : error ? (
<div className="text-center">
<p className="mb-4 text-zinc-700 dark:text-zinc-300">Error: {error}</p>
<Button onClick={loadMetadata} variant="outline">
Retry
{retryAttempt > 0 && (
<p className="text-xs text-zinc-400 dark:text-zinc-500 mb-4">
Attempted {retryAttempt + 1} times
</p>
)}
{backgroundRetryInterval && (
<p className="text-xs text-blue-500 dark:text-blue-400 mb-4 flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
Retrying in background...
</p>
)}
<Button
onClick={() => loadMetadata()}
variant="outline"
disabled={isLoading}
>
{isLoading ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Retrying...
</>
) : (
'Try Again'
)}
</Button>
</div>
) : currentSlideData ? (

View File

@ -71,7 +71,9 @@ export function PresentationViewer({
const [isLoadingMetadata, setIsLoadingMetadata] = useState(false);
const [error, setError] = useState<string | null>(null);
const [retryAttempt, setRetryAttempt] = useState(0);
const [hasScrolledToCurrentSlide, setHasScrolledToCurrentSlide] = useState(false);
const [backgroundRetryInterval, setBackgroundRetryInterval] = useState<NodeJS.Timeout | null>(null);
const [visibleSlide, setVisibleSlide] = useState<number | null>(null);
const [isFullScreenOpen, setIsFullScreenOpen] = useState(false);
@ -134,12 +136,13 @@ export function PresentationViewer({
return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase();
};
// Load metadata.json for the presentation
const loadMetadata = async () => {
// Load metadata.json for the presentation with retry logic
const loadMetadata = async (retryCount = 0, maxRetries = 5) => {
if (!extractedPresentationName || !project?.sandbox?.sandbox_url) return;
setIsLoadingMetadata(true);
setError(null);
setRetryAttempt(retryCount);
try {
// Sanitize the presentation name to match backend directory creation
@ -153,6 +156,8 @@ export function PresentationViewer({
// Add cache-busting parameter to ensure fresh data
const urlWithCacheBust = `${metadataUrl}?t=${Date.now()}`;
console.log(`Loading presentation metadata (attempt ${retryCount + 1}/${maxRetries + 1}):`, urlWithCacheBust);
const response = await fetch(urlWithCacheBust, {
cache: 'no-cache',
headers: {
@ -163,23 +168,67 @@ export function PresentationViewer({
if (response.ok) {
const data = await response.json();
setMetadata(data);
console.log('Successfully loaded presentation metadata:', data);
setIsLoadingMetadata(false);
// Clear background retry interval on success
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
setBackgroundRetryInterval(null);
}
return; // Success, exit early
} else {
setError('Failed to load presentation metadata');
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
} catch (err) {
console.error('Error loading metadata:', err);
setError('Failed to load presentation metadata');
} finally {
console.error(`Error loading metadata (attempt ${retryCount + 1}):`, err);
// If we haven't reached max retries, try again with exponential backoff
if (retryCount < maxRetries) {
const delay = Math.min(1000 * Math.pow(2, retryCount), 10000); // Cap at 10 seconds
console.log(`Retrying in ${delay}ms...`);
setTimeout(() => {
loadMetadata(retryCount + 1, maxRetries);
}, delay);
return; // Don't set error state yet, we're retrying
}
// All retries exhausted, set error and start background retry
setError('Failed to load presentation metadata after multiple attempts');
setIsLoadingMetadata(false);
// Start background retry every 10 seconds
if (!backgroundRetryInterval) {
const interval = setInterval(() => {
console.log('Background retry attempt...');
loadMetadata(0, 2); // Fewer retries for background attempts
}, 10000);
setBackgroundRetryInterval(interval);
}
}
};
useEffect(() => {
// Clear any existing background retry when dependencies change
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
setBackgroundRetryInterval(null);
}
loadMetadata();
}, [extractedPresentationName, project?.sandbox?.sandbox_url, toolContent]);
// Cleanup background retry interval on unmount
useEffect(() => {
return () => {
if (backgroundRetryInterval) {
clearInterval(backgroundRetryInterval);
}
};
}, [backgroundRetryInterval]);
// Reset scroll state when tool content changes (new tool call)
useEffect(() => {
setHasScrolledToCurrentSlide(false);
@ -580,7 +629,7 @@ export function PresentationViewer({
iconColor="text-blue-500 dark:text-blue-400"
bgColor="bg-gradient-to-b from-blue-100 to-blue-50 shadow-inner dark:from-blue-800/40 dark:to-blue-900/60 dark:shadow-blue-950/20"
title="Loading presentation"
filePath="Loading slides..."
filePath={retryAttempt > 0 ? `Retrying... (attempt ${retryAttempt + 1})` : "Loading slides..."}
showProgress={true}
/>
) : error || toolExecutionError || !metadata ? (
@ -595,6 +644,35 @@ export function PresentationViewer({
{toolExecutionError ? 'The presentation tool encountered an error during execution:' :
(error || 'There was an error loading the presentation. Please try again.')}
</p>
{retryAttempt > 0 && !toolExecutionError && (
<p className="text-xs text-zinc-400 dark:text-zinc-500 mb-4">
Attempted {retryAttempt + 1} times
</p>
)}
{backgroundRetryInterval && !toolExecutionError && (
<p className="text-xs text-blue-500 dark:text-blue-400 mb-4 flex items-center gap-1">
<Loader2 className="h-3 w-3 animate-spin" />
Retrying in background...
</p>
)}
{!toolExecutionError && error && (
<Button
onClick={() => loadMetadata()}
variant="outline"
size="sm"
disabled={isLoadingMetadata}
className="mb-4"
>
{isLoadingMetadata ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
Retrying...
</>
) : (
'Try Again'
)}
</Button>
)}
{toolExecutionError && (
<div className="w-full max-w-2xl">
<CodeBlockCode

View File

@ -1,6 +1,6 @@
import * as React from 'react';
const MOBILE_BREAKPOINT = 768;
const MOBILE_BREAKPOINT = 1024;
export function useIsMobile() {
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(

View File

@ -188,9 +188,17 @@ export function useAgentStream(
(finalStatus: string, runId: string | null = agentRunId) => {
if (!isMountedRef.current) return;
console.log(`[useAgentStream] Finalizing stream with status: ${finalStatus}, runId: ${runId}`);
const currentThreadId = threadIdRef.current; // Get current threadId from ref
const currentSetMessages = setMessagesRef.current; // Get current setMessages from ref
// Only finalize if this is for the current run ID or if no specific run ID is provided
if (runId && currentRunIdRef.current && currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Ignoring finalization for old run ID ${runId}, current is ${currentRunIdRef.current}`);
return;
}
if (streamCleanupRef.current) {
streamCleanupRef.current();
streamCleanupRef.current = null;
@ -423,6 +431,8 @@ export function useAgentStream(
if (!isMountedRef.current) return;
const runId = currentRunIdRef.current;
console.log(`[useAgentStream] Stream closed for run ID: ${runId}, status: ${status}`);
if (!runId) {
console.warn('[useAgentStream] Stream closed but no active agentRunId.');
// If status was streaming, something went wrong, finalize as error
@ -443,10 +453,19 @@ export function useAgentStream(
// Immediately check the agent status when the stream closes unexpectedly
// This covers cases where the agent finished but the final message wasn't received,
// or if the agent errored out on the backend.
console.log(`[useAgentStream] Checking final status for run ID: ${runId}`);
getAgentStatus(runId)
.then((agentStatus) => {
if (!isMountedRef.current) return; // Check mount status again
// Check if this is still the current run ID
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Run ID changed during status check in handleStreamClose, ignoring`);
return;
}
console.log(`[useAgentStream] Final status for run ID ${runId}: ${agentStatus.status}`);
if (agentStatus.status === 'running') {
setError('Stream closed unexpectedly while agent was running.');
finalizeStream('error', runId); // Finalize as error for now
@ -460,6 +479,12 @@ export function useAgentStream(
.catch((err) => {
if (!isMountedRef.current) return;
// Check if this is still the current run ID
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Run ID changed during error handling in handleStreamClose, ignoring`);
return;
}
const errorMessage = err instanceof Error ? err.message : String(err);
console.error(
`[useAgentStream] Error checking agent status for ${runId} after stream close: ${errorMessage}`,
@ -500,8 +525,11 @@ export function useAgentStream(
async (runId: string) => {
if (!isMountedRef.current) return;
console.log(`[useAgentStream] Starting stream for run ID: ${runId}`);
// Clean up any previous stream
if (streamCleanupRef.current) {
console.log(`[useAgentStream] Cleaning up previous stream`);
streamCleanupRef.current();
streamCleanupRef.current = null;
}
@ -516,38 +544,57 @@ export function useAgentStream(
try {
// *** Crucial check: Verify agent is running BEFORE connecting ***
console.log(`[useAgentStream] Checking status for run ID: ${runId}`);
const agentStatus = await getAgentStatus(runId);
if (!isMountedRef.current) return; // Check mount status after async call
// Check if this is still the current run ID we're trying to start
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Run ID changed during status check, aborting stream for ${runId}`);
return;
}
if (agentStatus.status !== 'running') {
console.warn(
`[useAgentStream] Agent run ${runId} is not in running state (status: ${agentStatus.status}). Cannot start stream.`,
);
setError(`Agent run is not running (status: ${agentStatus.status})`);
finalizeStream(
mapAgentStatus(agentStatus.status) || 'agent_not_running',
runId,
);
// Only set error and finalize if this is still the current run ID
if (currentRunIdRef.current === runId) {
setError(`Agent run is not running (status: ${agentStatus.status})`);
finalizeStream(
mapAgentStatus(agentStatus.status) || 'agent_not_running',
runId,
);
}
return;
}
console.log(`[useAgentStream] Agent run ${runId} is running, creating stream`);
// Agent is running, proceed to create the stream
const cleanup = streamAgent(runId, {
onMessage: (data) => {
// Ignore messages if threadId changed while the EventSource stayed open
if (threadIdRef.current !== threadId) return;
// Ignore messages if this is not the current run ID
if (currentRunIdRef.current !== runId) return;
handleStreamMessage(data);
},
onError: (err) => {
if (threadIdRef.current !== threadId) return;
if (currentRunIdRef.current !== runId) return;
handleStreamError(err);
},
onClose: () => {
if (threadIdRef.current !== threadId) return;
if (currentRunIdRef.current !== runId) return;
handleStreamClose();
},
});
streamCleanupRef.current = cleanup;
console.log(`[useAgentStream] Stream created successfully for run ID: ${runId}`);
// Status will be updated to 'streaming' by the first message received in handleStreamMessage
// If for some reason no message arrives shortly, verify liveness again to avoid zombie state
setTimeout(async () => {
@ -568,6 +615,12 @@ export function useAgentStream(
} catch (err) {
if (!isMountedRef.current) return; // Check mount status after async call
// Only handle error if this is still the current run ID
if (currentRunIdRef.current !== runId) {
console.log(`[useAgentStream] Error occurred for old run ID ${runId}, ignoring`);
return;
}
const errorMessage = err instanceof Error ? err.message : String(err);
console.error(
`[useAgentStream] Error initiating stream for ${runId}: ${errorMessage}`,
@ -583,6 +636,7 @@ export function useAgentStream(
}
},
[
threadId,
updateStatus,
finalizeStream,
handleStreamMessage,