mirror of https://github.com/kortix-ai/suna.git
Compare commits
5 Commits
504ac13e18
...
967b68feb9
Author | SHA1 | Date |
---|---|---|
|
967b68feb9 | |
|
14a86b753b | |
|
ebdddbb580 | |
|
e5d4223d4d | |
|
0b0efdf5eb |
|
@ -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
|
- **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`
|
- **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 Integration & Visual Content
|
||||||
|
|
||||||
### Image Sources & Integration
|
### Image Sources & Integration
|
||||||
|
|
|
@ -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:
|
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."""
|
"""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:
|
try:
|
||||||
# Get start of current month in UTC
|
# 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)}")
|
logger.error(f"[USAGE_LOGS] user_id={user_id} - Error fetching threads batch at offset {offset}: {str(thread_error)}")
|
||||||
raise
|
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:
|
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}
|
return {"logs": [], "has_more": False}
|
||||||
|
|
||||||
thread_ids = [t['thread_id'] for t in all_threads]
|
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")
|
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Database query for usage logs took {execution_time:.3f} seconds")
|
||||||
|
|
||||||
if not messages_result.data:
|
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}
|
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
|
# Get the user's subscription tier info for credit checking
|
||||||
logger.debug(f"[USAGE_LOGS] user_id={user_id} - Getting subscription info")
|
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)}")
|
logger.error(f"[USAGE_LOGS] user_id={user_id} - Error processing usage log entry for message {message.get('message_id', 'unknown')}: {str(e)}")
|
||||||
continue
|
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
|
# Check if there are more results
|
||||||
has_more = len(processed_logs) == items_per_page
|
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"
|
"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
|
return result
|
||||||
|
|
||||||
except Exception as outer_error:
|
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:
|
if subscription.get('items') and subscription['items'].get('data') and len(subscription['items']['data']) > 0:
|
||||||
price_id = subscription['items']['data'][0]['price']['id']
|
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)
|
# Check if it's one of the highest tier price IDs (200h/$1000 only)
|
||||||
highest_tier_price_ids = [
|
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
|
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
|
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
|
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)
|
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
|
# Clear cache for this user
|
||||||
await Cache.delete(f"monthly_usage:{user_id}")
|
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)
|
current_user_id: str = Depends(get_current_user_id_from_jwt)
|
||||||
):
|
):
|
||||||
"""Get detailed usage logs for a user with pagination."""
|
"""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:
|
try:
|
||||||
# Get Supabase client
|
# 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']}")
|
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']}")
|
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
|
return result
|
||||||
|
|
||||||
except HTTPException:
|
except HTTPException:
|
||||||
|
|
|
@ -119,12 +119,22 @@ export default function AgentsPage() {
|
||||||
return params;
|
return params;
|
||||||
}, [agentsPage, agentsSearchQuery, agentsSortBy, agentsSortOrder, agentsFilters]);
|
}, [agentsPage, agentsSearchQuery, agentsSortBy, agentsSortOrder, agentsFilters]);
|
||||||
|
|
||||||
const marketplaceQueryParams = useMemo(() => ({
|
const marketplaceQueryParams = useMemo(() => {
|
||||||
limit: 20,
|
const params: any = {
|
||||||
offset: (marketplacePage - 1) * 20,
|
limit: 20,
|
||||||
search: marketplaceSearchQuery || undefined,
|
offset: (marketplacePage - 1) * 20,
|
||||||
tags: marketplaceSelectedTags.length > 0 ? marketplaceSelectedTags.join(',') : undefined,
|
search: marketplaceSearchQuery || undefined,
|
||||||
}), [marketplacePage, marketplaceSearchQuery, marketplaceSelectedTags]);
|
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: agentsResponse, isLoading: agentsLoading, error: agentsError, refetch: loadAgents } = useAgents(agentsQueryParams);
|
||||||
const { data: marketplaceTemplates, isLoading: marketplaceLoading } = useMarketplaceTemplates(marketplaceQueryParams);
|
const { data: marketplaceTemplates, isLoading: marketplaceLoading } = useMarketplaceTemplates(marketplaceQueryParams);
|
||||||
|
@ -144,9 +154,8 @@ export default function AgentsPage() {
|
||||||
const agents = agentsResponse?.agents || [];
|
const agents = agentsResponse?.agents || [];
|
||||||
const agentsPagination = agentsResponse?.pagination;
|
const agentsPagination = agentsResponse?.pagination;
|
||||||
|
|
||||||
const { kortixTeamItems, communityItems, mineItems } = useMemo(() => {
|
const { allMarketplaceItems, mineItems } = useMemo(() => {
|
||||||
const kortixItems: MarketplaceTemplate[] = [];
|
const allItems: MarketplaceTemplate[] = [];
|
||||||
const communityItems: MarketplaceTemplate[] = [];
|
|
||||||
const mineItems: MarketplaceTemplate[] = [];
|
const mineItems: MarketplaceTemplate[] = [];
|
||||||
|
|
||||||
if (marketplaceTemplates) {
|
if (marketplaceTemplates) {
|
||||||
|
@ -168,61 +177,20 @@ export default function AgentsPage() {
|
||||||
metadata: template.metadata,
|
metadata: template.metadata,
|
||||||
};
|
};
|
||||||
|
|
||||||
const matchesSearch = !marketplaceSearchQuery.trim() || (() => {
|
// Backend handles search filtering, so we just transform the data
|
||||||
const searchLower = marketplaceSearchQuery.toLowerCase();
|
allItems.push(item);
|
||||||
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;
|
|
||||||
|
|
||||||
if (user?.id === template.creator_id) {
|
if (user?.id === template.creator_id) {
|
||||||
mineItems.push(item);
|
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 {
|
return {
|
||||||
kortixTeamItems: sortItems(kortixItems),
|
allMarketplaceItems: allItems,
|
||||||
communityItems: sortItems(communityItems),
|
mineItems: mineItems
|
||||||
mineItems: sortItems(mineItems)
|
|
||||||
};
|
};
|
||||||
}, [marketplaceTemplates, marketplaceSortBy, user?.id, marketplaceSearchQuery]);
|
}, [marketplaceTemplates, user?.id]);
|
||||||
|
|
||||||
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]);
|
|
||||||
|
|
||||||
const handleTabChange = (newTab: string) => {
|
const handleTabChange = (newTab: string) => {
|
||||||
const params = new URLSearchParams(searchParams.toString());
|
const params = new URLSearchParams(searchParams.toString());
|
||||||
|
@ -611,9 +579,7 @@ export default function AgentsPage() {
|
||||||
marketplaceFilter={marketplaceFilter}
|
marketplaceFilter={marketplaceFilter}
|
||||||
setMarketplaceFilter={setMarketplaceFilter}
|
setMarketplaceFilter={setMarketplaceFilter}
|
||||||
marketplaceLoading={marketplaceLoading}
|
marketplaceLoading={marketplaceLoading}
|
||||||
allMarketplaceItems={allMarketplaceItems}
|
allMarketplaceItems={marketplaceFilter === 'mine' ? mineItems : allMarketplaceItems}
|
||||||
kortixTeamItems={kortixTeamItems}
|
|
||||||
communityItems={communityItems}
|
|
||||||
mineItems={mineItems}
|
mineItems={mineItems}
|
||||||
installingItemId={installingItemId}
|
installingItemId={installingItemId}
|
||||||
onInstallClick={handleInstallClick}
|
onInstallClick={handleInstallClick}
|
||||||
|
|
|
@ -701,7 +701,7 @@ export default function ThreadPage({
|
||||||
className={cn(
|
className={cn(
|
||||||
"fixed bottom-0 z-10 bg-gradient-to-t from-background via-background/90 to-transparent px-4 pt-8",
|
"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",
|
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',
|
isSidePanelOpen && !isMobile ? 'right-[90%] sm:right-[450px] md:right-[500px] lg:right-[550px] xl:right-[650px]' : 'right-0',
|
||||||
isMobile ? 'left-0 right-0' : ''
|
isMobile ? 'left-0 right-0' : ''
|
||||||
)}>
|
)}>
|
||||||
|
|
|
@ -100,13 +100,8 @@ export function AgentModelSelector({
|
||||||
|
|
||||||
const normalizedValue = normalizeModelId(value);
|
const normalizedValue = normalizeModelId(value);
|
||||||
|
|
||||||
useEffect(() => {
|
// Use the prop value if provided, otherwise fall back to store value
|
||||||
if (normalizedValue && normalizedValue !== storeSelectedModel) {
|
const selectedModel = normalizedValue || storeSelectedModel;
|
||||||
storeHandleModelChange(normalizedValue);
|
|
||||||
}
|
|
||||||
}, [normalizedValue, storeSelectedModel, storeHandleModelChange]);
|
|
||||||
|
|
||||||
const selectedModel = storeSelectedModel;
|
|
||||||
|
|
||||||
const enhancedModelOptions = useMemo(() => {
|
const enhancedModelOptions = useMemo(() => {
|
||||||
const modelMap = new Map();
|
const modelMap = new Map();
|
||||||
|
@ -202,7 +197,6 @@ export function AgentModelSelector({
|
||||||
const isCustomModel = customModels.some(model => model.id === modelId);
|
const isCustomModel = customModels.some(model => model.id === modelId);
|
||||||
|
|
||||||
if (isCustomModel && isLocalMode()) {
|
if (isCustomModel && isLocalMode()) {
|
||||||
storeHandleModelChange(modelId);
|
|
||||||
onChange(modelId);
|
onChange(modelId);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
return;
|
return;
|
||||||
|
@ -210,7 +204,6 @@ export function AgentModelSelector({
|
||||||
|
|
||||||
const hasAccess = isLocalMode() || canAccessModel(modelId);
|
const hasAccess = isLocalMode() || canAccessModel(modelId);
|
||||||
if (hasAccess) {
|
if (hasAccess) {
|
||||||
storeHandleModelChange(modelId);
|
|
||||||
onChange(modelId);
|
onChange(modelId);
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
} else {
|
} else {
|
||||||
|
@ -285,12 +278,10 @@ export function AgentModelSelector({
|
||||||
|
|
||||||
if (dialogMode === 'add') {
|
if (dialogMode === 'add') {
|
||||||
storeAddCustomModel(newModel);
|
storeAddCustomModel(newModel);
|
||||||
storeHandleModelChange(modelId);
|
|
||||||
onChange(modelId);
|
onChange(modelId);
|
||||||
} else {
|
} else {
|
||||||
storeUpdateCustomModel(editingModelId!, newModel);
|
storeUpdateCustomModel(editingModelId!, newModel);
|
||||||
if (selectedModel === editingModelId) {
|
if (selectedModel === editingModelId) {
|
||||||
storeHandleModelChange(modelId);
|
|
||||||
onChange(modelId);
|
onChange(modelId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -312,7 +303,6 @@ export function AgentModelSelector({
|
||||||
|
|
||||||
if (selectedModel === modelId) {
|
if (selectedModel === modelId) {
|
||||||
const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
|
const defaultModel = subscriptionStatus === 'active' ? DEFAULT_PREMIUM_MODEL_ID : DEFAULT_FREE_MODEL_ID;
|
||||||
storeHandleModelChange(defaultModel);
|
|
||||||
onChange(defaultModel);
|
onChange(defaultModel);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -327,9 +317,8 @@ export function AgentModelSelector({
|
||||||
const isRecommended = MODELS[model.id]?.recommended || false;
|
const isRecommended = MODELS[model.id]?.recommended || false;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider key={`model-${model.id}-${index}`}>
|
<Tooltip key={`model-${model.id}-${index}`}>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
|
@ -402,16 +391,14 @@ export function AgentModelSelector({
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
) : null}
|
) : null}
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative">
|
<div className="relative">
|
||||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||||
{variant === 'menu-item' ? (
|
{variant === 'menu-item' ? (
|
||||||
<div
|
<div
|
||||||
|
@ -463,8 +450,7 @@ export function AgentModelSelector({
|
||||||
<TooltipContent side={variant === 'menu-item' ? 'left' : 'top'} className="text-xs">
|
<TooltipContent side={variant === 'menu-item' ? 'left' : 'top'} className="text-xs">
|
||||||
<p>Choose a model for this agent</p>
|
<p>Choose a model for this agent</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
|
||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
align={variant === 'menu-item' ? 'end' : 'start'}
|
align={variant === 'menu-item' ? 'end' : 'start'}
|
||||||
className="w-76 p-0 overflow-hidden"
|
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>
|
<span className="text-xs font-medium text-muted-foreground p-2 px-4">All Models</span>
|
||||||
{isLocalMode() && (
|
{isLocalMode() && (
|
||||||
<div className="flex items-center gap-1 p-2">
|
<div className="flex items-center gap-1 p-2">
|
||||||
<TooltipProvider>
|
<Tooltip>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Link
|
<Link
|
||||||
href="/settings/env-manager"
|
href="/settings/env-manager"
|
||||||
className="h-6 w-6 p-0 flex items-center justify-center"
|
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">
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
Local .Env Manager
|
Local .Env Manager
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
<Tooltip>
|
||||||
<TooltipProvider>
|
<TooltipTrigger asChild>
|
||||||
<Tooltip>
|
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<Button
|
<Button
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
|
@ -509,8 +492,7 @@ export function AgentModelSelector({
|
||||||
<TooltipContent side="bottom" className="text-xs">
|
<TooltipContent side="bottom" className="text-xs">
|
||||||
Add a custom model
|
Add a custom model
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
@ -549,9 +531,8 @@ export function AgentModelSelector({
|
||||||
const isRecommended = model.recommended;
|
const isRecommended = model.recommended;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider key={`premium-${model.id}-${index}`}>
|
<Tooltip key={`premium-${model.id}-${index}`}>
|
||||||
<Tooltip>
|
<TooltipTrigger asChild>
|
||||||
<TooltipTrigger asChild>
|
|
||||||
<div className='w-full'>
|
<div className='w-full'>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className={cn(
|
className={cn(
|
||||||
|
@ -586,7 +567,6 @@ export function AgentModelSelector({
|
||||||
</p>
|
</p>
|
||||||
</TooltipContent>
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{subscriptionStatus !== 'active' && (
|
{subscriptionStatus !== 'active' && (
|
||||||
|
|
|
@ -16,8 +16,6 @@ interface MarketplaceTabProps {
|
||||||
setMarketplaceFilter: (value: 'all' | 'kortix' | 'community' | 'mine') => void;
|
setMarketplaceFilter: (value: 'all' | 'kortix' | 'community' | 'mine') => void;
|
||||||
marketplaceLoading: boolean;
|
marketplaceLoading: boolean;
|
||||||
allMarketplaceItems: MarketplaceTemplate[];
|
allMarketplaceItems: MarketplaceTemplate[];
|
||||||
kortixTeamItems: MarketplaceTemplate[];
|
|
||||||
communityItems: MarketplaceTemplate[];
|
|
||||||
mineItems: MarketplaceTemplate[];
|
mineItems: MarketplaceTemplate[];
|
||||||
installingItemId: string | null;
|
installingItemId: string | null;
|
||||||
onInstallClick: (item: MarketplaceTemplate, e?: React.MouseEvent) => void;
|
onInstallClick: (item: MarketplaceTemplate, e?: React.MouseEvent) => void;
|
||||||
|
@ -34,8 +32,6 @@ export const MarketplaceTab = ({
|
||||||
setMarketplaceFilter,
|
setMarketplaceFilter,
|
||||||
marketplaceLoading,
|
marketplaceLoading,
|
||||||
allMarketplaceItems,
|
allMarketplaceItems,
|
||||||
kortixTeamItems,
|
|
||||||
communityItems,
|
|
||||||
mineItems,
|
mineItems,
|
||||||
installingItemId,
|
installingItemId,
|
||||||
onInstallClick,
|
onInstallClick,
|
||||||
|
@ -101,55 +97,27 @@ export const MarketplaceTab = ({
|
||||||
) : (
|
) : (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
{marketplaceFilter === 'all' ? (
|
{marketplaceFilter === 'all' ? (
|
||||||
<>
|
<div className="space-y-6">
|
||||||
{kortixTeamItems.length > 0 && (
|
{/* <MarketplaceSectionHeader
|
||||||
<div className="space-y-6">
|
title="Popular Agents"
|
||||||
<MarketplaceSectionHeader
|
subtitle="Sorted by popularity - most downloads first"
|
||||||
title="By team Kortix"
|
/> */}
|
||||||
subtitle="Official agents, maintained and supported"
|
<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) => (
|
</div>
|
||||||
<AgentCard
|
</div>
|
||||||
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 className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||||
{allMarketplaceItems.map((item) => (
|
{allMarketplaceItems.map((item) => (
|
||||||
|
|
|
@ -60,6 +60,8 @@ export function FullScreenPresentationViewer({
|
||||||
const [currentSlide, setCurrentSlide] = useState(initialSlide);
|
const [currentSlide, setCurrentSlide] = useState(initialSlide);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
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 [showControls, setShowControls] = useState(true);
|
||||||
const [showEditor, setShowEditor] = useState(false);
|
const [showEditor, setShowEditor] = useState(false);
|
||||||
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
|
const [isDownloadingPDF, setIsDownloadingPDF] = useState(false);
|
||||||
|
@ -78,12 +80,13 @@ export function FullScreenPresentationViewer({
|
||||||
return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase();
|
return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load metadata
|
// Load metadata with retry logic
|
||||||
const loadMetadata = useCallback(async () => {
|
const loadMetadata = useCallback(async (retryCount = 0, maxRetries = 5) => {
|
||||||
if (!presentationName || !sandboxUrl) return;
|
if (!presentationName || !sandboxUrl) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setRetryAttempt(retryCount);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Sanitize the presentation name to match backend directory creation
|
// Sanitize the presentation name to match backend directory creation
|
||||||
|
@ -94,7 +97,10 @@ export function FullScreenPresentationViewer({
|
||||||
`presentations/${sanitizedPresentationName}/metadata.json`
|
`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',
|
cache: 'no-cache',
|
||||||
headers: { 'Cache-Control': 'no-cache' }
|
headers: { 'Cache-Control': 'no-cache' }
|
||||||
});
|
});
|
||||||
|
@ -102,23 +108,75 @@ export function FullScreenPresentationViewer({
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setMetadata(data);
|
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 {
|
} else {
|
||||||
setError('Failed to load presentation metadata');
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading metadata:', err);
|
console.error(`Error loading metadata (attempt ${retryCount + 1}):`, err);
|
||||||
setError('Failed to load presentation metadata');
|
|
||||||
} finally {
|
// 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);
|
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(() => {
|
useEffect(() => {
|
||||||
if (isOpen) {
|
if (isOpen) {
|
||||||
|
// Clear any existing background retry when opening
|
||||||
|
if (backgroundRetryInterval) {
|
||||||
|
clearInterval(backgroundRetryInterval);
|
||||||
|
setBackgroundRetryInterval(null);
|
||||||
|
}
|
||||||
loadMetadata();
|
loadMetadata();
|
||||||
setCurrentSlide(initialSlide);
|
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
|
// Reload metadata when exiting editor mode to refresh with latest changes
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -402,13 +460,37 @@ export function FullScreenPresentationViewer({
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<div className="text-center">
|
<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>
|
<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>
|
</div>
|
||||||
) : error ? (
|
) : error ? (
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<p className="mb-4 text-zinc-700 dark:text-zinc-300">Error: {error}</p>
|
<p className="mb-4 text-zinc-700 dark:text-zinc-300">Error: {error}</p>
|
||||||
<Button onClick={loadMetadata} variant="outline">
|
{retryAttempt > 0 && (
|
||||||
Retry
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
) : currentSlideData ? (
|
) : currentSlideData ? (
|
||||||
|
|
|
@ -71,7 +71,9 @@ export function PresentationViewer({
|
||||||
|
|
||||||
const [isLoadingMetadata, setIsLoadingMetadata] = useState(false);
|
const [isLoadingMetadata, setIsLoadingMetadata] = useState(false);
|
||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
const [retryAttempt, setRetryAttempt] = useState(0);
|
||||||
const [hasScrolledToCurrentSlide, setHasScrolledToCurrentSlide] = useState(false);
|
const [hasScrolledToCurrentSlide, setHasScrolledToCurrentSlide] = useState(false);
|
||||||
|
const [backgroundRetryInterval, setBackgroundRetryInterval] = useState<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const [visibleSlide, setVisibleSlide] = useState<number | null>(null);
|
const [visibleSlide, setVisibleSlide] = useState<number | null>(null);
|
||||||
const [isFullScreenOpen, setIsFullScreenOpen] = useState(false);
|
const [isFullScreenOpen, setIsFullScreenOpen] = useState(false);
|
||||||
|
@ -134,12 +136,13 @@ export function PresentationViewer({
|
||||||
return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase();
|
return name.replace(/[^a-zA-Z0-9\-_]/g, '').toLowerCase();
|
||||||
};
|
};
|
||||||
|
|
||||||
// Load metadata.json for the presentation
|
// Load metadata.json for the presentation with retry logic
|
||||||
const loadMetadata = async () => {
|
const loadMetadata = async (retryCount = 0, maxRetries = 5) => {
|
||||||
if (!extractedPresentationName || !project?.sandbox?.sandbox_url) return;
|
if (!extractedPresentationName || !project?.sandbox?.sandbox_url) return;
|
||||||
|
|
||||||
setIsLoadingMetadata(true);
|
setIsLoadingMetadata(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
setRetryAttempt(retryCount);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Sanitize the presentation name to match backend directory creation
|
// Sanitize the presentation name to match backend directory creation
|
||||||
|
@ -153,6 +156,8 @@ export function PresentationViewer({
|
||||||
// Add cache-busting parameter to ensure fresh data
|
// Add cache-busting parameter to ensure fresh data
|
||||||
const urlWithCacheBust = `${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, {
|
const response = await fetch(urlWithCacheBust, {
|
||||||
cache: 'no-cache',
|
cache: 'no-cache',
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -163,23 +168,67 @@ export function PresentationViewer({
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
const data = await response.json();
|
const data = await response.json();
|
||||||
setMetadata(data);
|
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 {
|
} else {
|
||||||
setError('Failed to load presentation metadata');
|
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error loading metadata:', err);
|
console.error(`Error loading metadata (attempt ${retryCount + 1}):`, err);
|
||||||
setError('Failed to load presentation metadata');
|
|
||||||
} finally {
|
// 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);
|
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(() => {
|
useEffect(() => {
|
||||||
|
// Clear any existing background retry when dependencies change
|
||||||
|
if (backgroundRetryInterval) {
|
||||||
|
clearInterval(backgroundRetryInterval);
|
||||||
|
setBackgroundRetryInterval(null);
|
||||||
|
}
|
||||||
loadMetadata();
|
loadMetadata();
|
||||||
}, [extractedPresentationName, project?.sandbox?.sandbox_url, toolContent]);
|
}, [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)
|
// Reset scroll state when tool content changes (new tool call)
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setHasScrolledToCurrentSlide(false);
|
setHasScrolledToCurrentSlide(false);
|
||||||
|
@ -580,7 +629,7 @@ export function PresentationViewer({
|
||||||
iconColor="text-blue-500 dark:text-blue-400"
|
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"
|
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"
|
title="Loading presentation"
|
||||||
filePath="Loading slides..."
|
filePath={retryAttempt > 0 ? `Retrying... (attempt ${retryAttempt + 1})` : "Loading slides..."}
|
||||||
showProgress={true}
|
showProgress={true}
|
||||||
/>
|
/>
|
||||||
) : error || toolExecutionError || !metadata ? (
|
) : error || toolExecutionError || !metadata ? (
|
||||||
|
@ -595,6 +644,35 @@ export function PresentationViewer({
|
||||||
{toolExecutionError ? 'The presentation tool encountered an error during execution:' :
|
{toolExecutionError ? 'The presentation tool encountered an error during execution:' :
|
||||||
(error || 'There was an error loading the presentation. Please try again.')}
|
(error || 'There was an error loading the presentation. Please try again.')}
|
||||||
</p>
|
</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 && (
|
{toolExecutionError && (
|
||||||
<div className="w-full max-w-2xl">
|
<div className="w-full max-w-2xl">
|
||||||
<CodeBlockCode
|
<CodeBlockCode
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
|
|
||||||
const MOBILE_BREAKPOINT = 768;
|
const MOBILE_BREAKPOINT = 1024;
|
||||||
|
|
||||||
export function useIsMobile() {
|
export function useIsMobile() {
|
||||||
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
const [isMobile, setIsMobile] = React.useState<boolean | undefined>(
|
||||||
|
|
|
@ -188,9 +188,17 @@ export function useAgentStream(
|
||||||
(finalStatus: string, runId: string | null = agentRunId) => {
|
(finalStatus: string, runId: string | null = agentRunId) => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
|
console.log(`[useAgentStream] Finalizing stream with status: ${finalStatus}, runId: ${runId}`);
|
||||||
|
|
||||||
const currentThreadId = threadIdRef.current; // Get current threadId from ref
|
const currentThreadId = threadIdRef.current; // Get current threadId from ref
|
||||||
const currentSetMessages = setMessagesRef.current; // Get current setMessages 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) {
|
if (streamCleanupRef.current) {
|
||||||
streamCleanupRef.current();
|
streamCleanupRef.current();
|
||||||
streamCleanupRef.current = null;
|
streamCleanupRef.current = null;
|
||||||
|
@ -423,6 +431,8 @@ export function useAgentStream(
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
const runId = currentRunIdRef.current;
|
const runId = currentRunIdRef.current;
|
||||||
|
console.log(`[useAgentStream] Stream closed for run ID: ${runId}, status: ${status}`);
|
||||||
|
|
||||||
if (!runId) {
|
if (!runId) {
|
||||||
console.warn('[useAgentStream] Stream closed but no active agentRunId.');
|
console.warn('[useAgentStream] Stream closed but no active agentRunId.');
|
||||||
// If status was streaming, something went wrong, finalize as error
|
// 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
|
// Immediately check the agent status when the stream closes unexpectedly
|
||||||
// This covers cases where the agent finished but the final message wasn't received,
|
// This covers cases where the agent finished but the final message wasn't received,
|
||||||
// or if the agent errored out on the backend.
|
// or if the agent errored out on the backend.
|
||||||
|
console.log(`[useAgentStream] Checking final status for run ID: ${runId}`);
|
||||||
getAgentStatus(runId)
|
getAgentStatus(runId)
|
||||||
.then((agentStatus) => {
|
.then((agentStatus) => {
|
||||||
if (!isMountedRef.current) return; // Check mount status again
|
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') {
|
if (agentStatus.status === 'running') {
|
||||||
setError('Stream closed unexpectedly while agent was running.');
|
setError('Stream closed unexpectedly while agent was running.');
|
||||||
finalizeStream('error', runId); // Finalize as error for now
|
finalizeStream('error', runId); // Finalize as error for now
|
||||||
|
@ -460,6 +479,12 @@ export function useAgentStream(
|
||||||
.catch((err) => {
|
.catch((err) => {
|
||||||
if (!isMountedRef.current) return;
|
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);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
console.error(
|
console.error(
|
||||||
`[useAgentStream] Error checking agent status for ${runId} after stream close: ${errorMessage}`,
|
`[useAgentStream] Error checking agent status for ${runId} after stream close: ${errorMessage}`,
|
||||||
|
@ -500,8 +525,11 @@ export function useAgentStream(
|
||||||
async (runId: string) => {
|
async (runId: string) => {
|
||||||
if (!isMountedRef.current) return;
|
if (!isMountedRef.current) return;
|
||||||
|
|
||||||
|
console.log(`[useAgentStream] Starting stream for run ID: ${runId}`);
|
||||||
|
|
||||||
// Clean up any previous stream
|
// Clean up any previous stream
|
||||||
if (streamCleanupRef.current) {
|
if (streamCleanupRef.current) {
|
||||||
|
console.log(`[useAgentStream] Cleaning up previous stream`);
|
||||||
streamCleanupRef.current();
|
streamCleanupRef.current();
|
||||||
streamCleanupRef.current = null;
|
streamCleanupRef.current = null;
|
||||||
}
|
}
|
||||||
|
@ -516,38 +544,57 @@ export function useAgentStream(
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// *** Crucial check: Verify agent is running BEFORE connecting ***
|
// *** Crucial check: Verify agent is running BEFORE connecting ***
|
||||||
|
console.log(`[useAgentStream] Checking status for run ID: ${runId}`);
|
||||||
const agentStatus = await getAgentStatus(runId);
|
const agentStatus = await getAgentStatus(runId);
|
||||||
if (!isMountedRef.current) return; // Check mount status after async call
|
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') {
|
if (agentStatus.status !== 'running') {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[useAgentStream] Agent run ${runId} is not in running state (status: ${agentStatus.status}). Cannot start stream.`,
|
`[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(
|
// Only set error and finalize if this is still the current run ID
|
||||||
mapAgentStatus(agentStatus.status) || 'agent_not_running',
|
if (currentRunIdRef.current === runId) {
|
||||||
runId,
|
setError(`Agent run is not running (status: ${agentStatus.status})`);
|
||||||
);
|
finalizeStream(
|
||||||
|
mapAgentStatus(agentStatus.status) || 'agent_not_running',
|
||||||
|
runId,
|
||||||
|
);
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(`[useAgentStream] Agent run ${runId} is running, creating stream`);
|
||||||
|
|
||||||
// Agent is running, proceed to create the stream
|
// Agent is running, proceed to create the stream
|
||||||
const cleanup = streamAgent(runId, {
|
const cleanup = streamAgent(runId, {
|
||||||
onMessage: (data) => {
|
onMessage: (data) => {
|
||||||
// Ignore messages if threadId changed while the EventSource stayed open
|
// Ignore messages if threadId changed while the EventSource stayed open
|
||||||
if (threadIdRef.current !== threadId) return;
|
if (threadIdRef.current !== threadId) return;
|
||||||
|
// Ignore messages if this is not the current run ID
|
||||||
|
if (currentRunIdRef.current !== runId) return;
|
||||||
handleStreamMessage(data);
|
handleStreamMessage(data);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
if (threadIdRef.current !== threadId) return;
|
if (threadIdRef.current !== threadId) return;
|
||||||
|
if (currentRunIdRef.current !== runId) return;
|
||||||
handleStreamError(err);
|
handleStreamError(err);
|
||||||
},
|
},
|
||||||
onClose: () => {
|
onClose: () => {
|
||||||
if (threadIdRef.current !== threadId) return;
|
if (threadIdRef.current !== threadId) return;
|
||||||
|
if (currentRunIdRef.current !== runId) return;
|
||||||
handleStreamClose();
|
handleStreamClose();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
streamCleanupRef.current = cleanup;
|
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
|
// 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
|
// If for some reason no message arrives shortly, verify liveness again to avoid zombie state
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
|
@ -568,6 +615,12 @@ export function useAgentStream(
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (!isMountedRef.current) return; // Check mount status after async call
|
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);
|
const errorMessage = err instanceof Error ? err.message : String(err);
|
||||||
console.error(
|
console.error(
|
||||||
`[useAgentStream] Error initiating stream for ${runId}: ${errorMessage}`,
|
`[useAgentStream] Error initiating stream for ${runId}: ${errorMessage}`,
|
||||||
|
@ -583,6 +636,7 @@ export function useAgentStream(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
|
threadId,
|
||||||
updateStatus,
|
updateStatus,
|
||||||
finalizeStream,
|
finalizeStream,
|
||||||
handleStreamMessage,
|
handleStreamMessage,
|
||||||
|
|
Loading…
Reference in New Issue