From 3ca9aed8de2b05796b6dac2abb187f78f9af93b7 Mon Sep 17 00:00:00 2001 From: Soumyadas15 Date: Thu, 26 Jun 2025 22:06:47 +0530 Subject: [PATCH] feat: make thread agent agnostic --- backend/agent/api.py | 20 + backend/mcp_local/secure_api.py | 1 + backend/mcp_local/template_manager.py | 116 ++-- .../20250102000000_agent_agnostic_threads.sql | 1 - .../20250626114642_kortix_team_agents.sql | 15 + .../agents/_components/agents-grid.tsx | 25 +- .../agents/_components/search-and-filters.tsx | 14 +- .../agents/_utils/get-agent-style.ts | 30 +- .../agents/new/[agentId]/layout.tsx | 6 - frontend/src/app/(dashboard)/agents/page.tsx | 82 +-- .../src/app/(dashboard)/marketplace/page.tsx | 621 +++++++++++++----- .../settings/credentials/layout.tsx | 6 - .../chat-input/chat-settings-dropdown.tsx | 33 +- .../src/components/ui/pixel-art-editor.tsx | 262 ++++++++ frontend/src/components/ui/pixel-avatar.tsx | 266 ++++++++ .../react-query/secure-mcp/use-secure-mcp.ts | 1 + 16 files changed, 1173 insertions(+), 326 deletions(-) delete mode 100644 backend/supabase/migrations/20250102000000_agent_agnostic_threads.sql create mode 100644 backend/supabase/migrations/20250626114642_kortix_team_agents.sql create mode 100644 frontend/src/components/ui/pixel-art-editor.tsx create mode 100644 frontend/src/components/ui/pixel-avatar.tsx diff --git a/backend/agent/api.py b/backend/agent/api.py index f02ccfac..a3db33ea 100644 --- a/backend/agent/api.py +++ b/backend/agent/api.py @@ -1870,6 +1870,7 @@ class MarketplaceAgent(BaseModel): creator_name: str avatar: Optional[str] avatar_color: Optional[str] + is_kortix_team: Optional[bool] = False class MarketplaceAgentsResponse(BaseModel): agents: List[MarketplaceAgent] @@ -1938,6 +1939,25 @@ async def get_marketplace_agents( if has_more: total_pages = page + 1 + # Add Kortix team identification + kortix_team_creators = [ + 'kortix', 'kortix team', 'suna team', 'official', 'kortix official' + ] + + for agent in agents_data: + creator_name = agent.get('creator_name', '').lower() + agent['is_kortix_team'] = any( + kortix_creator in creator_name + for kortix_creator in kortix_team_creators + ) + + agents_data = sorted(agents_data, key=lambda x: ( + not x.get('is_kortix_team', False), + -x.get('download_count', 0) if sort_by == "most_downloaded" else 0, + x.get('name', '').lower() if sort_by == "name" else '', + -(datetime.fromisoformat(x.get('marketplace_published_at', x.get('created_at', ''))).timestamp()) if sort_by == "newest" else 0 + )) + logger.info(f"Found {len(agents_data)} marketplace agents (page {page}, estimated {total_pages} pages)") return { "agents": agents_data, diff --git a/backend/mcp_local/secure_api.py b/backend/mcp_local/secure_api.py index 67d63a7e..82d35a87 100644 --- a/backend/mcp_local/secure_api.py +++ b/backend/mcp_local/secure_api.py @@ -114,6 +114,7 @@ class TemplateResponse(BaseModel): creator_name: Optional[str] = None avatar: Optional[str] avatar_color: Optional[str] + is_kortix_team: Optional[bool] = False class InstallationResponse(BaseModel): """Response model for template installation""" diff --git a/backend/mcp_local/template_manager.py b/backend/mcp_local/template_manager.py index 262b731b..4b270049 100644 --- a/backend/mcp_local/template_manager.py +++ b/backend/mcp_local/template_manager.py @@ -71,24 +71,18 @@ class TemplateManager: tags: Optional[List[str]] = None ) -> str: """ - Create an agent template from an existing agent, stripping all credentials + Create a secure template from an existing agent - Args: - agent_id: ID of the existing agent - creator_id: ID of the user creating the template - make_public: Whether to make the template public immediately - tags: Optional tags for the template - - Returns: - template_id: ID of the created template + This extracts the agent configuration and creates a template with + MCP requirements (without credentials) that can be safely shared """ - logger.info(f"Creating template from agent {agent_id}") + logger.info(f"Creating template from agent {agent_id} for user {creator_id}") try: client = await db.client - # Get the existing agent with current version - agent_result = await client.table('agents').select('*, agent_versions!current_version_id(*)').eq('agent_id', agent_id).execute() + # Get the agent + agent_result = await client.table('agents').select('*').eq('agent_id', agent_id).execute() if not agent_result.data: raise ValueError("Agent not found") @@ -96,61 +90,63 @@ class TemplateManager: # Verify ownership if agent['account_id'] != creator_id: - raise ValueError("Access denied: not agent owner") + raise ValueError("Access denied - you can only create templates from your own agents") - # Extract MCP requirements (remove credentials) + # Extract MCP requirements from agent configuration mcp_requirements = [] - # Process configured_mcps - for mcp_config in agent.get('configured_mcps', []): - requirement = { - 'qualified_name': mcp_config.get('qualifiedName'), - 'display_name': mcp_config.get('name'), - 'enabled_tools': mcp_config.get('enabledTools', []), - 'required_config': list(mcp_config.get('config', {}).keys()) - } - mcp_requirements.append(requirement) + # Process configured_mcps (regular MCP servers) + for mcp in agent.get('configured_mcps', []): + if isinstance(mcp, dict) and 'qualifiedName' in mcp: + # Extract required config keys from the config + config_keys = list(mcp.get('config', {}).keys()) + + requirement = { + 'qualified_name': mcp['qualifiedName'], + 'display_name': mcp.get('name', mcp['qualifiedName']), + 'enabled_tools': mcp.get('enabledTools', []), + 'required_config': config_keys + } + mcp_requirements.append(requirement) - # Process custom_mcps + # Process custom_mcps (custom MCP servers) for custom_mcp in agent.get('custom_mcps', []): - custom_type = custom_mcp.get('customType', custom_mcp.get('type', 'sse')) - requirement = { - 'qualified_name': f"custom_{custom_type}_{custom_mcp['name'].replace(' ', '_').lower()}", - 'display_name': custom_mcp['name'], - 'enabled_tools': custom_mcp.get('enabledTools', []), - 'required_config': list(custom_mcp.get('config', {}).keys()), - 'custom_type': custom_type - } - logger.info(f"Created custom MCP requirement: {requirement}") - mcp_requirements.append(requirement) + if isinstance(custom_mcp, dict) and 'name' in custom_mcp: + # Extract required config keys from the config + config_keys = list(custom_mcp.get('config', {}).keys()) + + requirement = { + 'qualified_name': custom_mcp['name'].lower().replace(' ', '_'), + 'display_name': custom_mcp['name'], + 'enabled_tools': custom_mcp.get('enabledTools', []), + 'required_config': config_keys, + 'custom_type': custom_mcp.get('type', 'http') # Default to http + } + mcp_requirements.append(requirement) - # Use version data if available, otherwise fall back to agent data - version_data = agent.get('agent_versions', {}) - if version_data: - system_prompt = version_data.get('system_prompt', agent['system_prompt']) - agentpress_tools = version_data.get('agentpress_tools', agent.get('agentpress_tools', {})) - version_name = version_data.get('version_name', 'v1') - else: - system_prompt = agent['system_prompt'] - agentpress_tools = agent.get('agentpress_tools', {}) - version_name = 'v1' + kortix_team_account_ids = [ + 'xxxxxxxx', + ] - # Create template + is_kortix_team = creator_id in kortix_team_account_ids + + # Create the template template_data = { 'creator_id': creator_id, 'name': agent['name'], 'description': agent.get('description'), - 'system_prompt': system_prompt, + 'system_prompt': agent['system_prompt'], 'mcp_requirements': mcp_requirements, - 'agentpress_tools': agentpress_tools, + 'agentpress_tools': agent.get('agentpress_tools', {}), 'tags': tags or [], 'is_public': make_public, + 'is_kortix_team': is_kortix_team, 'avatar': agent.get('avatar'), 'avatar_color': agent.get('avatar_color'), 'metadata': { 'source_agent_id': agent_id, 'source_version_id': agent.get('current_version_id'), - 'source_version_name': version_name + 'source_version_name': agent.get('current_version', {}).get('version_name', 'v1.0') } } @@ -163,7 +159,7 @@ class TemplateManager: raise ValueError("Failed to create template") template_id = result.data[0]['template_id'] - logger.info(f"Successfully created template {template_id} from agent {agent_id}") + logger.info(f"Successfully created template {template_id} from agent {agent_id} with is_kortix_team={is_kortix_team}") return template_id @@ -554,10 +550,19 @@ class TemplateManager: if not template or template.creator_id != creator_id: raise ValueError("Template not found or access denied") + # Check if this is a Kortix team account + kortix_team_account_ids = [ + 'bc14b70b-2edf-473c-95be-5f5b109d6553', # Your Kortix team account ID + # Add more Kortix team account IDs here as needed + ] + + is_kortix_team = template.creator_id in kortix_team_account_ids + # Update template update_data = { 'is_public': True, - 'marketplace_published_at': datetime.now(timezone.utc).isoformat() + 'marketplace_published_at': datetime.now(timezone.utc).isoformat(), + 'is_kortix_team': is_kortix_team # Set based on account } if tags: @@ -568,6 +573,7 @@ class TemplateManager: .eq('template_id', template_id)\ .execute() + logger.info(f"Published template {template_id} with is_kortix_team={is_kortix_team}") return len(result.data) > 0 except Exception as e: @@ -615,6 +621,7 @@ class TemplateManager: query = client.table('agent_templates')\ .select('*')\ .eq('is_public', True)\ + .order('is_kortix_team', desc=True)\ .order('marketplace_published_at', desc=True)\ .range(offset, offset + limit - 1) @@ -628,6 +635,9 @@ class TemplateManager: templates = [] for template_data in result.data: + # Use the database field for is_kortix_team + is_kortix_team = template_data.get('is_kortix_team', False) + templates.append({ 'template_id': template_data['template_id'], 'name': template_data['name'], @@ -639,11 +649,13 @@ class TemplateManager: 'download_count': template_data.get('download_count', 0), 'marketplace_published_at': template_data.get('marketplace_published_at'), 'created_at': template_data['created_at'], - 'creator_name': 'Anonymous', + 'creator_name': 'Kortix Team' if is_kortix_team else 'Community', 'avatar': template_data.get('avatar'), - 'avatar_color': template_data.get('avatar_color') + 'avatar_color': template_data.get('avatar_color'), + 'is_kortix_team': is_kortix_team }) + # Templates are already sorted by database query (Kortix team first, then by date) return templates except Exception as e: diff --git a/backend/supabase/migrations/20250102000000_agent_agnostic_threads.sql b/backend/supabase/migrations/20250102000000_agent_agnostic_threads.sql deleted file mode 100644 index 0519ecba..00000000 --- a/backend/supabase/migrations/20250102000000_agent_agnostic_threads.sql +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/backend/supabase/migrations/20250626114642_kortix_team_agents.sql b/backend/supabase/migrations/20250626114642_kortix_team_agents.sql new file mode 100644 index 00000000..25f4a4be --- /dev/null +++ b/backend/supabase/migrations/20250626114642_kortix_team_agents.sql @@ -0,0 +1,15 @@ +-- Migration: Add is_kortix_team field to agent_templates +-- This migration adds support for marking templates as Kortix team templates + +BEGIN; + +-- Add is_kortix_team column to agent_templates table +ALTER TABLE agent_templates ADD COLUMN IF NOT EXISTS is_kortix_team BOOLEAN DEFAULT false; + +-- Create index for better performance +CREATE INDEX IF NOT EXISTS idx_agent_templates_is_kortix_team ON agent_templates(is_kortix_team); + +-- Add comment +COMMENT ON COLUMN agent_templates.is_kortix_team IS 'Indicates if this template is created by the Kortix team (official templates)'; + +COMMIT; \ No newline at end of file diff --git a/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx b/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx index 67fde4c2..021bdd7a 100644 --- a/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/agents-grid.tsx @@ -118,8 +118,6 @@ const AgentModal = ({ agent, isOpen, onClose, onCustomize, onChat, onPublish, on Chat - - {/* Marketplace Actions */}
{agent.is_public ? (
@@ -265,31 +263,20 @@ export const AgentsGrid = ({ const { avatar, color } = getAgentStyling(agent); const isPublishing = publishingId === agent.agent_id; const isUnpublishing = unpublishingId === agent.agent_id; - return (
handleAgentClick(agent)} > -
-
- {avatar} -
-
- {agent.is_default && ( - - )} - {agent.is_public && ( -
- - {agent.download_count || 0} -
- )} +
+
+
+ {avatar} +
- -
+

{agent.name} diff --git a/frontend/src/app/(dashboard)/agents/_components/search-and-filters.tsx b/frontend/src/app/(dashboard)/agents/_components/search-and-filters.tsx index c25fa576..4c740a59 100644 --- a/frontend/src/app/(dashboard)/agents/_components/search-and-filters.tsx +++ b/frontend/src/app/(dashboard)/agents/_components/search-and-filters.tsx @@ -49,9 +49,9 @@ export const SearchAndFilters = ({ allTools }: SearchAndFiltersProps) => { return ( -
-
-
+
+
+
- setSortBy(value)}> @@ -90,11 +90,11 @@ export const SearchAndFilters = ({ className="px-3" > {sortOrder === 'asc' ? : } - + */}
- + {/* - +
+
+
+
+ +

+ Agents +

+
+

+ Create and manage your agents with custom instructions and tools +

+
+
- - - void; + onInstall: (item: MarketplaceTemplate) => void; + isInstalling: boolean; +} + interface InstallDialogProps { item: MarketplaceTemplate | null; open: boolean; @@ -82,6 +92,159 @@ interface InstallDialogProps { isInstalling: boolean; } +const AgentPreviewSheet: React.FC = ({ + item, + open, + onOpenChange, + onInstall, + isInstalling +}) => { + if (!item) return null; + + const { avatar, color } = item.avatar && item.avatar_color + ? { avatar: item.avatar, color: item.avatar_color } + : getAgentAvatar(item.id); + + const formatDate = (dateString: string) => { + return new Date(dateString).toLocaleDateString('en-US', { + year: 'numeric', + month: 'long', + day: 'numeric' + }); + }; + + return ( + + + +
+
+
{avatar}
+
+
+
+ + {item.name} + +
+
+
+ + {item.creator_name} +
+
+ + {item.download_count} downloads +
+
+
+
+ +
+
+
+

+ Description +

+

+ {item.description || 'No description available for this agent.'} +

+
+ + {item.tags && item.tags.length > 0 && ( +
+

+ Tags +

+
+ {item.tags.map(tag => ( + + + {tag} + + ))} +
+
+ )} + {item.mcp_requirements && item.mcp_requirements.length > 0 && ( +
+

+ Required Tools & MCPs +

+
+ {item.mcp_requirements.map((mcp, index) => ( +
+
+
+ +
+
+
{mcp.display_name}
+ {mcp.enabled_tools && mcp.enabled_tools.length > 0 && ( +
+ {mcp.enabled_tools.length} tool{mcp.enabled_tools.length !== 1 ? 's' : ''} +
+ )} +
+
+ {mcp.custom_type && ( + + {mcp.custom_type.toUpperCase()} + + )} +
+ ))} +
+
+ )} + {item.metadata?.source_version_name && ( +
+

+ Version +

+
+ + {item.metadata.source_version_name} +
+
+ )} + {item.marketplace_published_at && ( +
+

+ Published +

+
+ + {formatDate(item.marketplace_published_at)} +
+
+ )} +
+
+
+ ); +}; + const InstallDialog: React.FC = ({ item, open, @@ -301,13 +464,13 @@ const InstallDialog: React.FC = ({ Missing Credential Profiles - This agent requires credential profiles for the following services: + This agent requires profiles for the following services:
{missingProfiles.map((profile) => ( - +
@@ -533,6 +696,7 @@ export default function MarketplacePage() { const [installingItemId, setInstallingItemId] = useState(null); const [selectedItem, setSelectedItem] = useState(null); const [showInstallDialog, setShowInstallDialog] = useState(false); + const [showPreviewSheet, setShowPreviewSheet] = useState(false); // Secure marketplace data (all templates are now secure) const secureQueryParams = useMemo(() => ({ @@ -546,13 +710,14 @@ export default function MarketplacePage() { const installTemplateMutation = useInstallTemplate(); // Transform secure templates data - const marketplaceItems = useMemo(() => { - const items: MarketplaceTemplate[] = []; + const { kortixTeamItems, communityItems } = useMemo(() => { + const kortixItems: MarketplaceTemplate[] = []; + const communityItems: MarketplaceTemplate[] = []; // Add secure templates (all items are now secure) if (secureTemplates) { secureTemplates.forEach(template => { - items.push({ + const item: MarketplaceTemplate = { id: template.template_id, name: template.name, description: template.description, @@ -564,35 +729,59 @@ export default function MarketplacePage() { avatar: template.avatar, avatar_color: template.avatar_color, template_id: template.template_id, + is_kortix_team: template.is_kortix_team, mcp_requirements: template.mcp_requirements, metadata: template.metadata, - }); + }; + + if (template.is_kortix_team) { + kortixItems.push(item); + } else { + communityItems.push(item); + } }); } - // Sort items - return items.sort((a, b) => { - switch (sortBy) { - 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; - } - }); + // Sort function + const sortItems = (items: MarketplaceTemplate[]) => { + return items.sort((a, b) => { + switch (sortBy) { + 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) + }; }, [secureTemplates, sortBy]); + // Combined items for tag filtering and search stats + const allMarketplaceItems = useMemo(() => { + return [...kortixTeamItems, ...communityItems]; + }, [kortixTeamItems, communityItems]); + React.useEffect(() => { setPage(1); }, [searchQuery, selectedTags, sortBy]); const handleItemClick = (item: MarketplaceTemplate) => { setSelectedItem(item); + setShowPreviewSheet(true); + }; + + const handlePreviewInstall = (item: MarketplaceTemplate) => { + setShowPreviewSheet(false); setShowInstallDialog(true); }; @@ -658,11 +847,11 @@ export default function MarketplacePage() { const allTags = React.useMemo(() => { const tags = new Set(); - marketplaceItems.forEach(item => { + allMarketplaceItems.forEach(item => { item.tags?.forEach(tag => tags.add(tag)); }); return Array.from(tags); - }, [marketplaceItems]); + }, [allMarketplaceItems]); if (flagLoading) { return ( @@ -701,54 +890,57 @@ export default function MarketplacePage() { return (
-
-
-

- Agent Marketplace -

-

- Discover and install secure AI agent templates created by the community -

+
+
+
+
+ +

+ Marketplace +

+
+

+ Discover and install powerful agents created by the community +

+
+
+
+ + setSearchQuery(e.target.value)} + className="pl-10" + /> +
+ {/* */} +
- -
-
- - setSearchQuery(e.target.value)} - className="pl-10" - /> -
- -
- {allTags.length > 0 && (

Filter by tags:

@@ -772,7 +964,7 @@ export default function MarketplacePage() { {isLoading ? ( "Loading marketplace..." ) : ( - `${marketplaceItems.length} template${marketplaceItems.length !== 1 ? 's' : ''} found` + `${allMarketplaceItems.length} template${allMarketplaceItems.length !== 1 ? 's' : ''} found` )}
@@ -792,7 +984,7 @@ export default function MarketplacePage() {
))}
- ) : marketplaceItems.length === 0 ? ( + ) : allMarketplaceItems.length === 0 ? (

{searchQuery || selectedTags.length > 0 @@ -801,95 +993,214 @@ export default function MarketplacePage() {

) : ( -
- {marketplaceItems.map((item) => { - const { avatar, color } = getItemStyling(item); - - return ( -
handleItemClick(item)} - > -
-
- {avatar} -
-
-
- - {item.download_count} -
-
+
+ {kortixTeamItems.length > 0 && ( +
+
+
+
-
-
-

- {item.name} -

- {item.metadata?.source_version_name && ( - - - {item.metadata.source_version_name} - - )} -
-

- {item.description || 'No description available'} -

- {item.tags && item.tags.length > 0 && ( -
- {item.tags.slice(0, 2).map(tag => ( - - {tag} - - ))} - {item.tags.length > 2 && ( - - +{item.tags.length - 2} - - )} -
- )} -
-
- - By {item.creator_name} -
- {item.marketplace_published_at && ( -
- - {new Date(item.marketplace_published_at).toLocaleDateString()} -
- )} -
- - +
+

Agents from Kortix Team

- ); - })} +
+ {kortixTeamItems.map((item) => { + const { avatar, color } = getItemStyling(item); + return ( +
handleItemClick(item)} + > +
+
+
+ {avatar} +
+
+
+
+
+

+ {item.name} +

+ {item.metadata?.source_version_name && ( + + + {item.metadata.source_version_name} + + )} +
+

+ {item.description || 'No description available'} +

+ {item.tags && item.tags.length > 0 && ( +
+ {item.tags.slice(0, 2).map(tag => ( + + {tag} + + ))} + {item.tags.length > 2 && ( + + +{item.tags.length - 2} + + )} +
+ )} +
+
+
+ + By {item.creator_name} +
+ {item.marketplace_published_at && ( +
+ + {new Date(item.marketplace_published_at).toLocaleDateString()} +
+ )} +
+
+ + {item.download_count} +
+
+ +
+
+ ); + })} +
+
+ )} + {communityItems.length > 0 && ( +
+
+
+ +
+
+

Agents from Community

+

Templates created by the community

+
+
+
+ {communityItems.map((item) => { + const { avatar, color } = getItemStyling(item); + + return ( +
handleItemClick(item)} + > +
+
+ {avatar} +
+
+
+ + {item.download_count} +
+
+
+
+
+

+ {item.name} +

+ {item.metadata?.source_version_name && ( + + + {item.metadata.source_version_name} + + )} +
+

+ {item.description || 'No description available'} +

+ {item.tags && item.tags.length > 0 && ( +
+ {item.tags.slice(0, 2).map(tag => ( + + {tag} + + ))} + {item.tags.length > 2 && ( + + +{item.tags.length - 2} + + )} +
+ )} +
+
+ + By {item.creator_name} +
+ {item.marketplace_published_at && ( +
+ + {new Date(item.marketplace_published_at).toLocaleDateString()} +
+ )} +
+ + +
+
+ ); + })} +
+
+ )}
)}
- + {children}; } diff --git a/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx b/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx index 0107cfa4..bfd52076 100644 --- a/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx +++ b/frontend/src/components/thread/chat-input/chat-settings-dropdown.tsx @@ -30,20 +30,20 @@ interface PredefinedAgent { } const PREDEFINED_AGENTS: PredefinedAgent[] = [ - { - id: 'slides', - name: 'Slides', - description: 'Create stunning presentations and slide decks', - icon: , - category: 'productivity' - }, - { - id: 'sheets', - name: 'Sheets', - description: 'Spreadsheet and data analysis expert', - icon: , - category: 'productivity' - } + // { + // id: 'slides', + // name: 'Slides', + // description: 'Create stunning presentations and slide decks', + // icon: , + // category: 'productivity' + // }, + // { + // id: 'sheets', + // name: 'Sheets', + // description: 'Spreadsheet and data analysis expert', + // icon: , + // category: 'productivity' + // } ]; interface ChatSettingsDropdownProps { @@ -213,7 +213,7 @@ export const ChatSettingsDropdown: React.FC = ({ />
-
+
{agentsLoading ? (
Loading agents... @@ -255,9 +255,6 @@ export const ChatSettingsDropdown: React.FC = ({ )}
- {/*

- {agent.description} -

*/}
{isSelected && ( diff --git a/frontend/src/components/ui/pixel-art-editor.tsx b/frontend/src/components/ui/pixel-art-editor.tsx new file mode 100644 index 00000000..d171ae94 --- /dev/null +++ b/frontend/src/components/ui/pixel-art-editor.tsx @@ -0,0 +1,262 @@ +import React, { useState, useCallback } from 'react'; +import { Button } from '@/components/ui/button'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Badge } from '@/components/ui/badge'; +import { Eraser, Palette, RotateCcw, Save } from 'lucide-react'; + +interface PixelArtEditorProps { + onSave: (pixelArt: string) => void; + onCancel: () => void; + initialPixelArt?: string; + size?: number; +} + +const GRID_SIZE = 16; +const COLORS = [ + '#000000', '#FFFFFF', '#FF0000', '#00FF00', '#0000FF', '#FFFF00', '#FF00FF', '#00FFFF', + '#800000', '#008000', '#000080', '#808000', '#800080', '#008080', '#C0C0C0', '#808080', + '#FF6B6B', '#4ECDC4', '#45B7D1', '#96CEB4', '#FECA57', '#FF9FF3', '#54A0FF', '#48DBFB' +]; + +export const PixelArtEditor: React.FC = ({ + onSave, + onCancel, + initialPixelArt, + size = 400 +}) => { + const [grid, setGrid] = useState(() => { + // Initialize grid with transparent pixels + const initialGrid = Array(GRID_SIZE).fill(null).map(() => + Array(GRID_SIZE).fill('transparent') + ); + + // If there's initial pixel art, try to parse it + if (initialPixelArt) { + try { + const parser = new DOMParser(); + const doc = parser.parseFromString(initialPixelArt, 'image/svg+xml'); + const rects = doc.querySelectorAll('rect'); + + rects.forEach(rect => { + const x = parseInt(rect.getAttribute('x') || '0'); + const y = parseInt(rect.getAttribute('y') || '0'); + const width = parseInt(rect.getAttribute('width') || '1'); + const height = parseInt(rect.getAttribute('height') || '1'); + const fill = rect.getAttribute('fill') || 'currentColor'; + + // Fill the grid based on the rect + for (let row = y; row < y + height && row < GRID_SIZE; row++) { + for (let col = x; col < x + width && col < GRID_SIZE; col++) { + if (row >= 0 && col >= 0) { + initialGrid[row][col] = fill; + } + } + } + }); + } catch (error) { + console.warn('Failed to parse initial pixel art:', error); + } + } + + return initialGrid; + }); + + const [selectedColor, setSelectedColor] = useState('#000000'); + const [isErasing, setIsErasing] = useState(false); + const [isDrawing, setIsDrawing] = useState(false); + + const handlePixelClick = useCallback((row: number, col: number) => { + setGrid(prev => { + const newGrid = prev.map(r => [...r]); + newGrid[row][col] = isErasing ? 'transparent' : selectedColor; + return newGrid; + }); + }, [selectedColor, isErasing]); + + const handleMouseDown = useCallback((row: number, col: number) => { + setIsDrawing(true); + handlePixelClick(row, col); + }, [handlePixelClick]); + + const handleMouseEnter = useCallback((row: number, col: number) => { + if (isDrawing) { + handlePixelClick(row, col); + } + }, [isDrawing, handlePixelClick]); + + const handleMouseUp = useCallback(() => { + setIsDrawing(false); + }, []); + + const clearGrid = useCallback(() => { + setGrid(Array(GRID_SIZE).fill(null).map(() => + Array(GRID_SIZE).fill('transparent') + )); + }, []); + + const generateSVG = useCallback(() => { + const rects: string[] = []; + const visited = Array(GRID_SIZE).fill(null).map(() => Array(GRID_SIZE).fill(false)); + for (let row = 0; row < GRID_SIZE; row++) { + for (let col = 0; col < GRID_SIZE; col++) { + if (!visited[row][col] && grid[row][col] !== 'transparent') { + const color = grid[row][col]; + let width = 1; + let height = 1; + while (col + width < GRID_SIZE && + grid[row][col + width] === color && + !visited[row][col + width]) { + width++; + } + let canExtendHeight = true; + while (row + height < GRID_SIZE && canExtendHeight) { + for (let c = col; c < col + width; c++) { + if (grid[row + height][c] !== color || visited[row + height][c]) { + canExtendHeight = false; + break; + } + } + if (canExtendHeight) height++; + } + for (let r = row; r < row + height; r++) { + for (let c = col; c < col + width; c++) { + visited[r][c] = true; + } + } + const fill = color === 'currentColor' ? 'currentColor' : color; + rects.push(``); + } + } + } + return `\n ${rects.join('\n ')}\n`; + }, [grid]); + + const handleSave = useCallback(() => { + const svg = generateSVG(); + onSave(svg); + }, [generateSVG, onSave]); + + const pixelSize = size / GRID_SIZE; + + return ( + + + + + Pixel Art Editor + + + +
+
+ Colors: + +
+
+ {COLORS.map(color => ( + +
+
+
+
+ {grid.map((row, rowIndex) => + row.map((pixel, colIndex) => ( +
handleMouseDown(rowIndex, colIndex)} + onMouseEnter={() => handleMouseEnter(rowIndex, colIndex)} + onMouseUp={handleMouseUp} + /> + )) + )} +
+
+
+ Preview: +
+
+
+
+
+
+
+ +
+ + +
+
+ + + ); +}; \ No newline at end of file diff --git a/frontend/src/components/ui/pixel-avatar.tsx b/frontend/src/components/ui/pixel-avatar.tsx new file mode 100644 index 00000000..60a4da63 --- /dev/null +++ b/frontend/src/components/ui/pixel-avatar.tsx @@ -0,0 +1,266 @@ +import React from 'react'; + +export const PIXEL_ART_DESIGNS = { + robot: ` + + + + + + + + + + + `, + + cat: ` + + + + + + + + + `, + + wizard: ` + + + + + + + + + + `, + + knight: ` + + + + + + + + `, + + ninja: ` + + + + + + + + `, + + pirate: ` + + + + + + + + `, + + alien: ` + + + + + + + + `, + + dragon: ` + + + + + + + + + `, + + ghost: ` + + + + + + + + + `, + + bear: ` + + + + + + + + + `, + + astronaut: ` + + + + + + + + + + `, + + viking: ` + + + + + + + + + + `, + + demon: ` + + + + + + + + + + + `, + + samurai: ` + + + + + + + + + `, + + witch: ` + + + + + + + + + `, + + cyborg: ` + + + + + + + + + ` +}; + +export const PIXEL_ART_CATEGORIES = { + fantasy: { + name: "Fantasy", + designs: ["wizard", "knight", "dragon", "witch", "demon"] + }, + scifi: { + name: "Sci-Fi", + designs: ["robot", "alien", "astronaut", "cyborg"] + }, + animals: { + name: "Animals", + designs: ["cat", "bear", "ghost"] + }, + warriors: { + name: "Warriors", + designs: ["ninja", "pirate", "viking", "samurai"] + } +}; + +interface PixelAvatarProps { + design: string; + size?: number; + className?: string; + customPixels?: string; +} + +export const PixelAvatar: React.FC = ({ + design, + size = 32, + className = "", + customPixels +}) => { + const pixelArt = customPixels || PIXEL_ART_DESIGNS[design]; + + if (!pixelArt) { + return ( +
+ + + + + + +
+ ); + } + + return ( +
+ ); +}; + +export const getAllPixelDesigns = (): string[] => { + return Object.keys(PIXEL_ART_DESIGNS); +}; + +export const getPixelDesignsByCategory = (category: string): string[] => { + return PIXEL_ART_CATEGORIES[category]?.designs || []; +}; + +export const getRandomPixelDesign = (): string => { + const designs = getAllPixelDesigns(); + return designs[Math.floor(Math.random() * designs.length)]; +}; + +export const getPixelDesignFromSeed = (seed: string): string => { + const designs = getAllPixelDesigns(); + let hash = 0; + for (let i = 0; i < seed.length; i++) { + const char = seed.charCodeAt(i); + hash = ((hash << 5) - hash) + char; + hash = hash & hash; + } + const designIndex = Math.abs(hash) % designs.length; + return designs[designIndex]; +}; \ No newline at end of file diff --git a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts index 9b548178..18173285 100644 --- a/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts +++ b/frontend/src/hooks/react-query/secure-mcp/use-secure-mcp.ts @@ -44,6 +44,7 @@ export interface AgentTemplate { creator_name?: string; avatar?: string; avatar_color?: string; + is_kortix_team?: boolean; metadata?: { source_agent_id?: string; source_version_id?: string;