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
|
||||
- **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
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -119,12 +119,22 @@ export default function AgentsPage() {
|
|||
return params;
|
||||
}, [agentsPage, agentsSearchQuery, agentsSortBy, agentsSortOrder, agentsFilters]);
|
||||
|
||||
const marketplaceQueryParams = useMemo(() => ({
|
||||
const marketplaceQueryParams = useMemo(() => {
|
||||
const params: any = {
|
||||
limit: 20,
|
||||
offset: (marketplacePage - 1) * 20,
|
||||
search: marketplaceSearchQuery || undefined,
|
||||
tags: marketplaceSelectedTags.length > 0 ? marketplaceSelectedTags.join(',') : undefined,
|
||||
}), [marketplacePage, marketplaceSearchQuery, marketplaceSelectedTags]);
|
||||
};
|
||||
|
||||
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}
|
||||
|
|
|
@ -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' : ''
|
||||
)}>
|
||||
|
|
|
@ -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,8 +317,7 @@ export function AgentModelSelector({
|
|||
const isRecommended = MODELS[model.id]?.recommended || false;
|
||||
|
||||
return (
|
||||
<TooltipProvider key={`model-${model.id}-${index}`}>
|
||||
<Tooltip>
|
||||
<Tooltip key={`model-${model.id}-${index}`}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<DropdownMenuItem
|
||||
|
@ -402,14 +391,12 @@ export function AgentModelSelector({
|
|||
</TooltipContent>
|
||||
) : null}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<DropdownMenu open={isOpen} onOpenChange={setIsOpen}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<DropdownMenuTrigger asChild disabled={disabled}>
|
||||
|
@ -464,7 +451,6 @@ export function AgentModelSelector({
|
|||
<p>Choose a model for this agent</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<DropdownMenuContent
|
||||
align={variant === 'menu-item' ? 'end' : 'start'}
|
||||
className="w-76 p-0 overflow-hidden"
|
||||
|
@ -476,7 +462,6 @@ 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>
|
||||
<Link
|
||||
|
@ -490,8 +475,6 @@ export function AgentModelSelector({
|
|||
Local .Env Manager
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button
|
||||
|
@ -510,7 +493,6 @@ export function AgentModelSelector({
|
|||
Add a custom model
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
@ -549,8 +531,7 @@ export function AgentModelSelector({
|
|||
const isRecommended = model.recommended;
|
||||
|
||||
return (
|
||||
<TooltipProvider key={`premium-${model.id}-${index}`}>
|
||||
<Tooltip>
|
||||
<Tooltip key={`premium-${model.id}-${index}`}>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='w-full'>
|
||||
<DropdownMenuItem
|
||||
|
@ -586,7 +567,6 @@ export function AgentModelSelector({
|
|||
</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
);
|
||||
})}
|
||||
{subscriptionStatus !== 'active' && (
|
||||
|
|
|
@ -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,15 +97,13 @@ 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"
|
||||
/>
|
||||
{/* <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">
|
||||
{kortixTeamItems.map((item) => (
|
||||
{allMarketplaceItems.map((item) => (
|
||||
<AgentCard
|
||||
key={item.id}
|
||||
mode="marketplace"
|
||||
|
@ -124,32 +118,6 @@ export const MarketplaceTab = ({
|
|||
))}
|
||||
</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">
|
||||
{allMarketplaceItems.map((item) => (
|
||||
|
|
|
@ -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 {
|
||||
setIsLoading(false);
|
||||
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
|
||||
}
|
||||
}, [presentationName, sandboxUrl]);
|
||||
|
||||
// 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, 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 ? (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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>(
|
||||
|
|
|
@ -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.`,
|
||||
);
|
||||
|
||||
// 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,
|
||||
|
|
Loading…
Reference in New Issue