mirror of https://github.com/kortix-ai/suna.git
feat: make thread agent agnostic
This commit is contained in:
parent
18057a4db6
commit
3ca9aed8de
|
@ -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,
|
||||
|
|
|
@ -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"""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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;
|
|
@ -118,8 +118,6 @@ const AgentModal = ({ agent, isOpen, onClose, onCustomize, onChat, onPublish, on
|
|||
Chat
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* Marketplace Actions */}
|
||||
<div className="pt-2 border-t">
|
||||
{agent.is_public ? (
|
||||
<div className="space-y-2">
|
||||
|
@ -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 (
|
||||
<div
|
||||
key={agent.agent_id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group"
|
||||
onClick={() => handleAgentClick(agent)}
|
||||
>
|
||||
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
|
||||
<div className="text-4xl">
|
||||
{avatar}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 flex gap-2">
|
||||
{agent.is_default && (
|
||||
<Star className="h-4 w-4 text-white fill-white drop-shadow-sm" />
|
||||
)}
|
||||
{agent.is_public && (
|
||||
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
|
||||
<Shield className="h-3 w-3 text-white" />
|
||||
<span className="text-white text-xs font-medium">{agent.download_count || 0}</span>
|
||||
</div>
|
||||
)}
|
||||
<div className='p-4'>
|
||||
<div className={`h-12 w-12 flex items-center justify-center rounded-lg`} style={{ backgroundColor: color }}>
|
||||
<div className="text-2xl">
|
||||
{avatar}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-4">
|
||||
<div className="p-4 -mt-4 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
|
||||
{agent.name}
|
||||
|
|
|
@ -49,9 +49,9 @@ export const SearchAndFilters = ({
|
|||
allTools
|
||||
}: SearchAndFiltersProps) => {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-col gap-3 sm:flex-row sm:items-center flex-1">
|
||||
<div className="relative flex-1 max-w-md">
|
||||
<div className="flex flex-col w-full gap-4 lg:flex-row lg:items-center lg:justify-between">
|
||||
<div className="flex flex-col w-full gap-3 sm:flex-row sm:items-center flex-1">
|
||||
<div className="relative flex-1 w-full border rounded-xl">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
|
@ -71,7 +71,7 @@ export const SearchAndFilters = ({
|
|||
)}
|
||||
</div>
|
||||
|
||||
<Select value={sortBy} onValueChange={(value: SortOption) => setSortBy(value)}>
|
||||
{/* <Select value={sortBy} onValueChange={(value: SortOption) => setSortBy(value)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
|
@ -90,11 +90,11 @@ export const SearchAndFilters = ({
|
|||
className="px-3"
|
||||
>
|
||||
{sortOrder === 'asc' ? <SortAsc className="h-4 w-4" /> : <SortDesc className="h-4 w-4" />}
|
||||
</Button>
|
||||
</Button> */}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<DropdownMenu>
|
||||
{/* <DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm" className="relative">
|
||||
<Filter className="h-4 w-4" />
|
||||
|
@ -137,7 +137,7 @@ export const SearchAndFilters = ({
|
|||
</>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</DropdownMenu> */}
|
||||
|
||||
{/* <div className="flex border rounded-md">
|
||||
<Button
|
||||
|
|
|
@ -1,10 +1,22 @@
|
|||
import { getPixelDesignFromSeed } from '@/components/ui/pixel-avatar';
|
||||
|
||||
export const getAgentAvatar = (agentId: string) => {
|
||||
const avatars = ['🤖', '🎯', '⚡', '🚀', '🔮', '🎨', '📊', '🔧', '💡', '🌟'];
|
||||
const colors = ['#06b6d4', '#22c55e', '#8b5cf6', '#3b82f6', '#ec4899', '#eab308', '#ef4444', '#6366f1'];
|
||||
const avatarIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % avatars.length;
|
||||
const colorIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length;
|
||||
return {
|
||||
avatar: avatars[avatarIndex],
|
||||
color: colors[colorIndex]
|
||||
};
|
||||
};
|
||||
const avatars = ['🤖', '🎯', '⚡', '🚀', '🔮', '🎨', '📊', '🔧', '💡', '🌟'];
|
||||
const colors = ['#06b6d4', '#22c55e', '#8b5cf6', '#3b82f6', '#ec4899', '#eab308', '#ef4444', '#6366f1'];
|
||||
const avatarIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % avatars.length;
|
||||
const colorIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length;
|
||||
return {
|
||||
avatar: avatars[avatarIndex],
|
||||
color: colors[colorIndex]
|
||||
};
|
||||
};
|
||||
|
||||
export const getAgentPixelAvatar = (agentId: string) => {
|
||||
const colors = ['#06b6d4', '#22c55e', '#8b5cf6', '#3b82f6', '#ec4899', '#eab308', '#ef4444', '#6366f1'];
|
||||
const colorIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length;
|
||||
const pixelDesign = getPixelDesignFromSeed(agentId);
|
||||
return {
|
||||
avatar: pixelDesign,
|
||||
color: colors[colorIndex]
|
||||
};
|
||||
};
|
|
@ -1,6 +1,4 @@
|
|||
import { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
import { isFlagEnabled } from '@/lib/feature-flags';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Create Agent | Kortix Suna',
|
||||
|
@ -17,9 +15,5 @@ export default async function NewAgentLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const agentPlaygroundEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!agentPlaygroundEnabled) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Plus, AlertCircle, Loader2, File } from 'lucide-react';
|
||||
import { Plus, AlertCircle, Loader2, File, Bot } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { UpdateAgentDialog } from './_components/update-agent-dialog';
|
||||
|
@ -240,60 +240,36 @@ export default function AgentsPage() {
|
|||
return (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="space-y-8">
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="space-y-1">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Your Agents
|
||||
</h1>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Create and manage your AI agents with custom instructions and tools
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<Button
|
||||
onClick={() => router.push('/marketplace/my-templates')}
|
||||
className="self-start sm:self-center"
|
||||
variant="outline"
|
||||
>
|
||||
<File className="h-5 w-5" />
|
||||
My Templates
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleCreateNewAgent}
|
||||
disabled={createAgentMutation.isPending}
|
||||
className="self-start sm:self-center"
|
||||
>
|
||||
{createAgentMutation.isPending ? (
|
||||
<>
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
Creating...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Plus className="h-5 w-5" />
|
||||
New Agent
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div className='w-full space-y-4 bg-gradient-to-b from-primary/10 to-primary/5 border rounded-xl h-60 flex items-center justify-center'>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2 text-center">
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<Bot className='h-6 w-6 text-primary' />
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Agents
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Create and manage your agents with custom instructions and tools
|
||||
</p>
|
||||
</div>
|
||||
<SearchAndFilters
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
activeFiltersCount={activeFiltersCount}
|
||||
clearFilters={clearFilters}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
allTools={allTools}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SearchAndFilters
|
||||
searchQuery={searchQuery}
|
||||
setSearchQuery={setSearchQuery}
|
||||
sortBy={sortBy}
|
||||
setSortBy={setSortBy}
|
||||
sortOrder={sortOrder}
|
||||
setSortOrder={setSortOrder}
|
||||
filters={filters}
|
||||
setFilters={setFilters}
|
||||
activeFiltersCount={activeFiltersCount}
|
||||
clearFilters={clearFilters}
|
||||
viewMode={viewMode}
|
||||
setViewMode={setViewMode}
|
||||
allTools={allTools}
|
||||
/>
|
||||
|
||||
<ResultsInfo
|
||||
isLoading={isLoading}
|
||||
totalAgents={pagination?.total || 0}
|
||||
|
|
|
@ -1,13 +1,14 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo, useEffect } from 'react';
|
||||
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle, GitBranch, Plus } from 'lucide-react';
|
||||
import { Search, Download, Star, Calendar, User, Tags, TrendingUp, Shield, CheckCircle, Loader2, Settings, Wrench, AlertTriangle, GitBranch, Plus, ShoppingBag } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Sheet, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle } from '@/components/ui/sheet';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { toast } from 'sonner';
|
||||
import { getAgentAvatar } from '../agents/_utils/get-agent-style';
|
||||
|
@ -37,6 +38,7 @@ interface MarketplaceTemplate {
|
|||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
template_id: string;
|
||||
is_kortix_team?: boolean;
|
||||
mcp_requirements?: Array<{
|
||||
qualified_name: string;
|
||||
display_name: string;
|
||||
|
@ -74,6 +76,14 @@ interface MissingProfile {
|
|||
required_config: string[];
|
||||
}
|
||||
|
||||
interface AgentPreviewSheetProps {
|
||||
item: MarketplaceTemplate | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => 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<AgentPreviewSheetProps> = ({
|
||||
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 (
|
||||
<Sheet open={open} onOpenChange={onOpenChange}>
|
||||
<SheetContent>
|
||||
<SheetHeader className="space-y-4">
|
||||
<div className="flex items-start gap-4">
|
||||
<div
|
||||
className="h-16 w-16 flex items-center justify-center rounded-xl shrink-0"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
<div className="text-3xl">{avatar}</div>
|
||||
</div>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<SheetTitle className="text-xl font-semibold line-clamp-2">
|
||||
{item.name}
|
||||
</SheetTitle>
|
||||
</div>
|
||||
<div className="flex items-center gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-1">
|
||||
<User className="h-4 w-4" />
|
||||
<span>{item.creator_name}</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-4 w-4" />
|
||||
<span>{item.download_count} downloads</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => onInstall(item)}
|
||||
disabled={isInstalling}
|
||||
size='sm'
|
||||
className='w-48'
|
||||
>
|
||||
{isInstalling ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Add to Library
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</SheetHeader>
|
||||
<div className="px-4 space-y-6 py-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Description
|
||||
</h3>
|
||||
<p className="text-sm leading-relaxed">
|
||||
{item.description || 'No description available for this agent.'}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">
|
||||
Tags
|
||||
</h3>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{item.tags.map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
<Tags className="h-3 w-3 mr-1" />
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.mcp_requirements && item.mcp_requirements.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Required Tools & MCPs
|
||||
</h3>
|
||||
<div className="space-y-2">
|
||||
{item.mcp_requirements.map((mcp, index) => (
|
||||
<div key={index} className="flex items-center justify-between p-3 bg-muted-foreground/10 border rounded-lg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-primary/10">
|
||||
<Wrench className="h-4 w-4 text-primary" />
|
||||
</div>
|
||||
<div>
|
||||
<div className="font-medium text-sm">{mcp.display_name}</div>
|
||||
{mcp.enabled_tools && mcp.enabled_tools.length > 0 && (
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{mcp.enabled_tools.length} tool{mcp.enabled_tools.length !== 1 ? 's' : ''}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{mcp.custom_type && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
{mcp.custom_type.toUpperCase()}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.metadata?.source_version_name && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-sm text-muted-foreground uppercase tracking-wide">
|
||||
Version
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<GitBranch className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">{item.metadata.source_version_name}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{item.marketplace_published_at && (
|
||||
<div className="space-y-2">
|
||||
<h3 className="font-medium text-xs text-muted-foreground uppercase tracking-wide">
|
||||
Published
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4 text-muted-foreground" />
|
||||
<span className="text-sm">{formatDate(item.marketplace_published_at)}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
};
|
||||
|
||||
const InstallDialog: React.FC<InstallDialogProps> = ({
|
||||
item,
|
||||
open,
|
||||
|
@ -301,13 +464,13 @@ const InstallDialog: React.FC<InstallDialogProps> = ({
|
|||
<AlertTriangle className="h-4 w-4 text-destructive" />
|
||||
<AlertTitle className="text-destructive">Missing Credential Profiles</AlertTitle>
|
||||
<AlertDescription className="text-destructive/80">
|
||||
This agent requires credential profiles for the following services:
|
||||
This agent requires profiles for the following services:
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
|
||||
<div className="space-y-3">
|
||||
{missingProfiles.map((profile) => (
|
||||
<Card key={profile.qualified_name} className="border-destructive/20 shadow-none bg-transparent">
|
||||
<Card key={profile.qualified_name} className="py-0 border-destructive/20 shadow-none bg-transparent">
|
||||
<CardContent className="flex items-center justify-between p-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-destructive/10">
|
||||
|
@ -533,6 +696,7 @@ export default function MarketplacePage() {
|
|||
const [installingItemId, setInstallingItemId] = useState<string | null>(null);
|
||||
const [selectedItem, setSelectedItem] = useState<MarketplaceTemplate | null>(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<string>();
|
||||
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 (
|
||||
<div className="container mx-auto max-w-7xl px-4 py-8">
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Agent Marketplace
|
||||
</h1>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Discover and install secure AI agent templates created by the community
|
||||
</p>
|
||||
<div className='w-full space-y-4 bg-gradient-to-b from-primary/10 to-primary/5 border rounded-xl h-60 flex items-center justify-center'>
|
||||
<div className="space-y-4">
|
||||
<div className="space-y-2 text-center">
|
||||
<div className='flex items-center justify-center gap-2'>
|
||||
<ShoppingBag className='h-6 w-6 text-primary' />
|
||||
<h1 className="text-2xl font-semibold tracking-tight text-foreground">
|
||||
Marketplace
|
||||
</h1>
|
||||
</div>
|
||||
<p className="text-md text-muted-foreground max-w-2xl">
|
||||
Discover and install powerful agents created by the community
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1 border rounded-xl">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search agents..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
{/* <Select value={sortBy} onValueChange={(value) => setSortBy(value as SortOption)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="newest">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Newest First
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="popular">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4" />
|
||||
Most Popular
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="most_downloaded">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
Most Downloaded
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select> */}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-4 sm:flex-row sm:items-center">
|
||||
<div className="relative flex-1">
|
||||
<Search className="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
|
||||
<Input
|
||||
placeholder="Search agent templates..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
<Select value={sortBy} onValueChange={(value) => setSortBy(value as SortOption)}>
|
||||
<SelectTrigger className="w-[180px]">
|
||||
<SelectValue placeholder="Sort by" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="newest">
|
||||
<div className="flex items-center gap-2">
|
||||
<Calendar className="h-4 w-4" />
|
||||
Newest First
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="popular">
|
||||
<div className="flex items-center gap-2">
|
||||
<Star className="h-4 w-4" />
|
||||
Most Popular
|
||||
</div>
|
||||
</SelectItem>
|
||||
<SelectItem value="most_downloaded">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="h-4 w-4" />
|
||||
Most Downloaded
|
||||
</div>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{allTags.length > 0 && (
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm font-medium text-muted-foreground">Filter by tags:</p>
|
||||
|
@ -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`
|
||||
)}
|
||||
</div>
|
||||
|
||||
|
@ -792,7 +984,7 @@ export default function MarketplacePage() {
|
|||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : marketplaceItems.length === 0 ? (
|
||||
) : allMarketplaceItems.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">
|
||||
{searchQuery || selectedTags.length > 0
|
||||
|
@ -801,95 +993,214 @@ export default function MarketplacePage() {
|
|||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{marketplaceItems.map((item) => {
|
||||
const { avatar, color } = getItemStyling(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
|
||||
<div className="text-4xl">
|
||||
{avatar}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 flex gap-2">
|
||||
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
|
||||
<Download className="h-3 w-3 text-white" />
|
||||
<span className="text-white text-xs font-medium">{item.download_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-12">
|
||||
{kortixTeamItems.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-blue-100 dark:bg-blue-900/20">
|
||||
<Shield className="h-4 w-4 text-blue-600 dark:text-blue-400" />
|
||||
</div>
|
||||
<div className="p-4 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.metadata?.source_version_name && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
<GitBranch className="h-3 w-3" />
|
||||
{item.metadata.source_version_name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
|
||||
{item.description || 'No description available'}
|
||||
</p>
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{item.tags.slice(0, 2).map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{item.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{item.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1 mb-4">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>By {item.creator_name}</span>
|
||||
</div>
|
||||
{item.marketplace_published_at && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={(e) => handleInstallClick(item, e)}
|
||||
disabled={installingItemId === item.id}
|
||||
className="w-full transition-opacity mt-auto"
|
||||
size="sm"
|
||||
>
|
||||
{installingItemId === item.id ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Template
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-foreground">Agents from Kortix Team</h2>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{kortixTeamItems.map((item) => {
|
||||
const { avatar, color } = getItemStyling(item);
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
<div className='p-4'>
|
||||
<div className={`h-12 w-12 flex items-center justify-center rounded-lg`} style={{ backgroundColor: color }}>
|
||||
<div className="text-2xl">
|
||||
{avatar}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 -mt-4 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.metadata?.source_version_name && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
<GitBranch className="h-3 w-3" />
|
||||
{item.metadata.source_version_name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
|
||||
{item.description || 'No description available'}
|
||||
</p>
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{item.tags.slice(0, 2).map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{item.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{item.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="mb-4 w-full flex justify-between">
|
||||
<div className='space-y-1'>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>By {item.creator_name}</span>
|
||||
</div>
|
||||
{item.marketplace_published_at && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<Download className="h-3 w-3 text-muted-foreground" />
|
||||
<span className="text-muted-foreground text-xs font-medium">{item.download_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
onClick={(e) => handleInstallClick(item, e)}
|
||||
disabled={installingItemId === item.id}
|
||||
className="w-full transition-opacity mt-auto"
|
||||
size="sm"
|
||||
>
|
||||
{installingItemId === item.id ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Add to Library
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{communityItems.length > 0 && (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="flex h-8 w-8 items-center justify-center rounded-full bg-green-100 dark:bg-green-900/20">
|
||||
<User className="h-4 w-4 text-green-600 dark:text-green-400" />
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-foreground">Agents from Community</h2>
|
||||
<p className="text-sm text-muted-foreground">Templates created by the community</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{communityItems.map((item) => {
|
||||
const { avatar, color } = getItemStyling(item);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={item.id}
|
||||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group flex flex-col h-full"
|
||||
onClick={() => handleItemClick(item)}
|
||||
>
|
||||
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
|
||||
<div className="text-4xl">
|
||||
{avatar}
|
||||
</div>
|
||||
<div className="absolute top-3 right-3 flex gap-2">
|
||||
<div className="flex items-center gap-1 bg-white/20 backdrop-blur-sm px-2 py-1 rounded-full">
|
||||
<Download className="h-3 w-3 text-white" />
|
||||
<span className="text-white text-xs font-medium">{item.download_count}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="p-4 flex flex-col flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<h3 className="text-foreground font-medium text-lg line-clamp-1 flex-1">
|
||||
{item.name}
|
||||
</h3>
|
||||
{item.metadata?.source_version_name && (
|
||||
<Badge variant="secondary" className="text-xs shrink-0">
|
||||
<GitBranch className="h-3 w-3" />
|
||||
{item.metadata.source_version_name}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-muted-foreground text-sm mb-3 line-clamp-2">
|
||||
{item.description || 'No description available'}
|
||||
</p>
|
||||
{item.tags && item.tags.length > 0 && (
|
||||
<div className="flex flex-wrap gap-1 mb-3">
|
||||
{item.tags.slice(0, 2).map(tag => (
|
||||
<Badge key={tag} variant="outline" className="text-xs">
|
||||
{tag}
|
||||
</Badge>
|
||||
))}
|
||||
{item.tags.length > 2 && (
|
||||
<Badge variant="outline" className="text-xs">
|
||||
+{item.tags.length - 2}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-1 mb-4">
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<User className="h-3 w-3" />
|
||||
<span>By {item.creator_name}</span>
|
||||
</div>
|
||||
{item.marketplace_published_at && (
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<Calendar className="h-3 w-3" />
|
||||
<span>{new Date(item.marketplace_published_at).toLocaleDateString()}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<Button
|
||||
onClick={(e) => handleInstallClick(item, e)}
|
||||
disabled={installingItemId === item.id}
|
||||
className="w-full transition-opacity mt-auto"
|
||||
size="sm"
|
||||
>
|
||||
{installingItemId === item.id ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 animate-spin mr-2" />
|
||||
Installing...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Download className="h-4 w-4" />
|
||||
Install Template
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<AgentPreviewSheet
|
||||
item={selectedItem}
|
||||
open={showPreviewSheet}
|
||||
onOpenChange={setShowPreviewSheet}
|
||||
onInstall={handlePreviewInstall}
|
||||
isInstalling={installingItemId === selectedItem?.id}
|
||||
/>
|
||||
<InstallDialog
|
||||
item={selectedItem}
|
||||
open={showInstallDialog}
|
||||
|
|
|
@ -1,6 +1,4 @@
|
|||
import { isFlagEnabled } from '@/lib/feature-flags';
|
||||
import { Metadata } from 'next';
|
||||
import { redirect } from 'next/navigation';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: 'Credentials | Kortix Suna',
|
||||
|
@ -17,9 +15,5 @@ export default async function CredentialsLayout({
|
|||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const customAgentsEnabled = await isFlagEnabled('custom_agents');
|
||||
if (!customAgentsEnabled) {
|
||||
redirect('/dashboard');
|
||||
}
|
||||
return <>{children}</>;
|
||||
}
|
||||
|
|
|
@ -30,20 +30,20 @@ interface PredefinedAgent {
|
|||
}
|
||||
|
||||
const PREDEFINED_AGENTS: PredefinedAgent[] = [
|
||||
{
|
||||
id: 'slides',
|
||||
name: 'Slides',
|
||||
description: 'Create stunning presentations and slide decks',
|
||||
icon: <Presentation className="h-4 w-4" />,
|
||||
category: 'productivity'
|
||||
},
|
||||
{
|
||||
id: 'sheets',
|
||||
name: 'Sheets',
|
||||
description: 'Spreadsheet and data analysis expert',
|
||||
icon: <FileSpreadsheet className="h-4 w-4" />,
|
||||
category: 'productivity'
|
||||
}
|
||||
// {
|
||||
// id: 'slides',
|
||||
// name: 'Slides',
|
||||
// description: 'Create stunning presentations and slide decks',
|
||||
// icon: <Presentation className="h-4 w-4" />,
|
||||
// category: 'productivity'
|
||||
// },
|
||||
// {
|
||||
// id: 'sheets',
|
||||
// name: 'Sheets',
|
||||
// description: 'Spreadsheet and data analysis expert',
|
||||
// icon: <FileSpreadsheet className="h-4 w-4" />,
|
||||
// category: 'productivity'
|
||||
// }
|
||||
];
|
||||
|
||||
interface ChatSettingsDropdownProps {
|
||||
|
@ -213,7 +213,7 @@ export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-h-80 overflow-y-auto">
|
||||
<div className="max-h-80 overflow-y-auto scrollbar-thin scrollbar-thumb-muted-foreground/20 scrollbar-track-transparent">
|
||||
{agentsLoading ? (
|
||||
<div className="p-3 text-sm text-muted-foreground text-center">
|
||||
Loading agents...
|
||||
|
@ -255,9 +255,6 @@ export const ChatSettingsDropdown: React.FC<ChatSettingsDropdownProps> = ({
|
|||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{/* <p className="text-xs text-muted-foreground line-clamp-1 mt-0.5">
|
||||
{agent.description}
|
||||
</p> */}
|
||||
</div>
|
||||
</div>
|
||||
{isSelected && (
|
||||
|
|
|
@ -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<PixelArtEditorProps> = ({
|
||||
onSave,
|
||||
onCancel,
|
||||
initialPixelArt,
|
||||
size = 400
|
||||
}) => {
|
||||
const [grid, setGrid] = useState<string[][]>(() => {
|
||||
// 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(`<rect x="${col}" y="${row}" width="${width}" height="${height}" fill="${fill}"/>`);
|
||||
}
|
||||
}
|
||||
}
|
||||
return `<svg viewBox="0 0 ${GRID_SIZE} ${GRID_SIZE}" xmlns="http://www.w3.org/2000/svg">\n ${rects.join('\n ')}\n</svg>`;
|
||||
}, [grid]);
|
||||
|
||||
const handleSave = useCallback(() => {
|
||||
const svg = generateSVG();
|
||||
onSave(svg);
|
||||
}, [generateSVG, onSave]);
|
||||
|
||||
const pixelSize = size / GRID_SIZE;
|
||||
|
||||
return (
|
||||
<Card className="w-full max-w-2xl mx-auto">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2">
|
||||
<Palette className="h-5 w-5" />
|
||||
Pixel Art Editor
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<div className="space-y-2">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium">Colors:</span>
|
||||
<Button
|
||||
variant={isErasing ? "default" : "outline"}
|
||||
size="sm"
|
||||
onClick={() => setIsErasing(!isErasing)}
|
||||
>
|
||||
<Eraser className="h-4 w-4" />
|
||||
{isErasing ? "Erasing" : "Eraser"}
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{COLORS.map(color => (
|
||||
<button
|
||||
key={color}
|
||||
className={`w-8 h-8 rounded border-2 ${
|
||||
selectedColor === color && !isErasing
|
||||
? 'border-primary ring-2 ring-primary/20'
|
||||
: 'border-border'
|
||||
}`}
|
||||
style={{ backgroundColor: color }}
|
||||
onClick={() => {
|
||||
setSelectedColor(color);
|
||||
setIsErasing(false);
|
||||
}}
|
||||
title={color}
|
||||
/>
|
||||
))}
|
||||
<button
|
||||
className={`w-8 h-8 rounded border-2 ${
|
||||
selectedColor === 'currentColor' && !isErasing
|
||||
? 'border-primary ring-2 ring-primary/20'
|
||||
: 'border-border'
|
||||
} bg-gradient-to-br from-blue-500 to-purple-500`}
|
||||
onClick={() => {
|
||||
setSelectedColor('currentColor');
|
||||
setIsErasing(false);
|
||||
}}
|
||||
title="Theme Color"
|
||||
>
|
||||
<span className="text-white text-xs font-bold">T</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center">
|
||||
<div
|
||||
className="grid border-2 border-border bg-white dark:bg-gray-900 relative"
|
||||
style={{
|
||||
gridTemplateColumns: `repeat(${GRID_SIZE}, 1fr)`,
|
||||
width: size,
|
||||
height: size
|
||||
}}
|
||||
onMouseLeave={handleMouseUp}
|
||||
>
|
||||
{grid.map((row, rowIndex) =>
|
||||
row.map((pixel, colIndex) => (
|
||||
<div
|
||||
key={`${rowIndex}-${colIndex}`}
|
||||
className="border border-gray-200 dark:border-gray-700 cursor-pointer hover:opacity-80"
|
||||
style={{
|
||||
backgroundColor: pixel === 'transparent' ? 'transparent' :
|
||||
pixel === 'currentColor' ? '#3b82f6' : pixel,
|
||||
width: pixelSize,
|
||||
height: pixelSize
|
||||
}}
|
||||
onMouseDown={() => handleMouseDown(rowIndex, colIndex)}
|
||||
onMouseEnter={() => handleMouseEnter(rowIndex, colIndex)}
|
||||
onMouseUp={handleMouseUp}
|
||||
/>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<span className="text-sm font-medium">Preview:</span>
|
||||
<div className="flex items-center gap-4">
|
||||
<div
|
||||
className="w-12 h-12 border rounded"
|
||||
style={{ backgroundColor: '#3b82f6', color: 'white' }}
|
||||
dangerouslySetInnerHTML={{ __html: generateSVG() }}
|
||||
/>
|
||||
<div
|
||||
className="w-12 h-12 border rounded"
|
||||
style={{ backgroundColor: '#ef4444', color: 'white' }}
|
||||
dangerouslySetInnerHTML={{ __html: generateSVG() }}
|
||||
/>
|
||||
<div
|
||||
className="w-12 h-12 border rounded"
|
||||
style={{ backgroundColor: '#10b981', color: 'white' }}
|
||||
dangerouslySetInnerHTML={{ __html: generateSVG() }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex gap-2 justify-between">
|
||||
<Button variant="outline" onClick={clearGrid}>
|
||||
<RotateCcw className="h-4 w-4" />
|
||||
Clear
|
||||
</Button>
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={onCancel}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSave}>
|
||||
<Save className="h-4 w-4" />
|
||||
Save
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
};
|
|
@ -0,0 +1,266 @@
|
|||
import React from 'react';
|
||||
|
||||
export const PIXEL_ART_DESIGNS = {
|
||||
robot: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="2" width="8" height="6" fill="currentColor"/>
|
||||
<rect x="3" y="3" width="1" height="4" fill="currentColor"/>
|
||||
<rect x="12" y="3" width="1" height="4" fill="currentColor"/>
|
||||
<rect x="5" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="9" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="6" y="5" width="1" height="1" fill="currentColor"/>
|
||||
<rect x="9" y="5" width="1" height="1" fill="currentColor"/>
|
||||
<rect x="2" y="9" width="12" height="4" fill="currentColor"/>
|
||||
<rect x="4" y="10" width="8" height="2" fill="white"/>
|
||||
<rect x="5" y="11" width="6" height="1" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
cat: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="6" y="2" width="4" height="6" fill="currentColor"/>
|
||||
<rect x="5" y="1" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="9" y="1" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="7" y="6" width="2" height="1" fill="white"/>
|
||||
<rect x="6" y="7" width="4" height="1" fill="white"/>
|
||||
</svg>`,
|
||||
|
||||
wizard: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="7" y="1" width="2" height="1" fill="currentColor"/>
|
||||
<rect x="6" y="2" width="4" height="1" fill="currentColor"/>
|
||||
<rect x="5" y="3" width="6" height="1" fill="currentColor"/>
|
||||
<rect x="4" y="4" width="8" height="4" fill="currentColor"/>
|
||||
<rect x="6" y="5" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="5" width="1" height="1" fill="white"/>
|
||||
<rect x="7" y="7" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
</svg>`,
|
||||
|
||||
knight: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="2" width="6" height="6" fill="currentColor"/>
|
||||
<rect x="4" y="3" width="8" height="4" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="2" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="2" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="7" y="1" width="2" height="2" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
ninja: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="5" y="6" width="6" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="2" y="2" width="12" height="1" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
pirate: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="8" y="4" width="3" height="1" fill="white"/>
|
||||
<rect x="7" y="6" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="3" y="2" width="10" height="1" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
alien: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="5" y="2" width="6" height="6" fill="currentColor"/>
|
||||
<rect x="4" y="3" width="8" height="4" fill="currentColor"/>
|
||||
<rect x="3" y="4" width="10" height="2" fill="currentColor"/>
|
||||
<rect x="5" y="5" width="2" height="2" fill="white"/>
|
||||
<rect x="9" y="5" width="2" height="2" fill="white"/>
|
||||
<rect x="2" y="8" width="12" height="4" fill="currentColor"/>
|
||||
<rect x="4" y="10" width="8" height="2" fill="white"/>
|
||||
</svg>`,
|
||||
|
||||
dragon: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="6" y="2" width="4" height="6" fill="currentColor"/>
|
||||
<rect x="5" y="3" width="6" height="4" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="4" y="1" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="10" y="1" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="3" y="8" width="10" height="4" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
</svg>`,
|
||||
|
||||
ghost: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="10" fill="currentColor"/>
|
||||
<rect x="5" y="2" width="6" height="1" fill="currentColor"/>
|
||||
<rect x="6" y="1" width="4" height="1" fill="currentColor"/>
|
||||
<rect x="6" y="5" width="1" height="2" fill="white"/>
|
||||
<rect x="9" y="5" width="1" height="2" fill="white"/>
|
||||
<rect x="4" y="13" width="2" height="1" fill="transparent"/>
|
||||
<rect x="7" y="13" width="2" height="1" fill="transparent"/>
|
||||
<rect x="10" y="13" width="2" height="1" fill="transparent"/>
|
||||
</svg>`,
|
||||
|
||||
bear: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="3" y="2" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="11" y="2" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="4" y="3" width="8" height="6" fill="currentColor"/>
|
||||
<rect x="6" y="5" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="5" width="1" height="1" fill="white"/>
|
||||
<rect x="7" y="7" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="9" width="10" height="4" fill="currentColor"/>
|
||||
<rect x="5" y="11" width="6" height="2" fill="white"/>
|
||||
</svg>`,
|
||||
|
||||
astronaut: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="2" width="8" height="6" fill="currentColor"/>
|
||||
<rect x="3" y="3" width="10" height="4" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="2" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="2" fill="white"/>
|
||||
<rect x="7" y="6" width="2" height="1" fill="white"/>
|
||||
<rect x="2" y="8" width="12" height="5" fill="currentColor"/>
|
||||
<rect x="4" y="10" width="8" height="2" fill="white"/>
|
||||
<rect x="1" y="9" width="2" height="2" fill="currentColor"/>
|
||||
<rect x="13" y="9" width="2" height="2" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
viking: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="7" y="6" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="2" y="1" width="3" height="3" fill="currentColor"/>
|
||||
<rect x="11" y="1" width="3" height="3" fill="currentColor"/>
|
||||
<rect x="7" y="0" width="2" height="2" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
demon: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="9" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="6" y="5" width="1" height="1" fill="currentColor"/>
|
||||
<rect x="9" y="5" width="1" height="1" fill="currentColor"/>
|
||||
<rect x="7" y="7" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="3" y="1" width="2" height="3" fill="currentColor"/>
|
||||
<rect x="11" y="1" width="2" height="3" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
samurai: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="5" y="6" width="6" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="6" y="1" width="4" height="2" fill="currentColor"/>
|
||||
<rect x="5" y="2" width="6" height="1" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
witch: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="3" width="8" height="5" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="9" y="4" width="1" height="1" fill="white"/>
|
||||
<rect x="7" y="6" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="6" y="0" width="4" height="3" fill="currentColor"/>
|
||||
<rect x="10" y="1" width="2" height="2" fill="currentColor"/>
|
||||
</svg>`,
|
||||
|
||||
cyborg: `<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="4" y="2" width="8" height="6" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="1" height="2" fill="white"/>
|
||||
<rect x="9" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="10" y="4" width="1" height="1" fill="currentColor"/>
|
||||
<rect x="7" y="6" width="2" height="1" fill="white"/>
|
||||
<rect x="3" y="8" width="10" height="5" fill="currentColor"/>
|
||||
<rect x="5" y="10" width="6" height="2" fill="white"/>
|
||||
<rect x="12" y="3" width="2" height="2" fill="currentColor"/>
|
||||
</svg>`
|
||||
};
|
||||
|
||||
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<PixelAvatarProps> = ({
|
||||
design,
|
||||
size = 32,
|
||||
className = "",
|
||||
customPixels
|
||||
}) => {
|
||||
const pixelArt = customPixels || PIXEL_ART_DESIGNS[design];
|
||||
|
||||
if (!pixelArt) {
|
||||
return (
|
||||
<div
|
||||
className={`inline-block ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<svg viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg" className="w-full h-full">
|
||||
<rect x="4" y="2" width="8" height="6" fill="currentColor"/>
|
||||
<rect x="6" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="9" y="4" width="2" height="2" fill="white"/>
|
||||
<rect x="2" y="9" width="12" height="4" fill="currentColor"/>
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`inline-block ${className}`}
|
||||
style={{ width: size, height: size }}
|
||||
dangerouslySetInnerHTML={{ __html: pixelArt }}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
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];
|
||||
};
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue