mirror of https://github.com/kortix-ai/suna.git
Compare commits
15 Commits
967b68feb9
...
7a67ebe746
Author | SHA1 | Date |
---|---|---|
|
7a67ebe746 | |
|
87d25e3634 | |
|
85abdf7698 | |
|
7ed528400c | |
|
47238cba3b | |
|
70df974720 | |
|
a7df37b0ba | |
|
dad49f8a50 | |
|
2e31dd4593 | |
|
e4189c74a3 | |
|
f55cc025d7 | |
|
ab32d5040f | |
|
7af07582ff | |
|
ce98bb8a99 | |
|
9ae810683d |
|
@ -18,7 +18,7 @@ from services import redis
|
|||
from utils.auth_utils import get_current_user_id_from_jwt, get_user_id_from_stream_auth, verify_thread_access, verify_admin_api_key
|
||||
from utils.logger import logger, structlog
|
||||
from services.billing import check_billing_status, can_use_model
|
||||
from utils.config import config
|
||||
from utils.config import config, EnvMode
|
||||
from sandbox.sandbox import create_sandbox, delete_sandbox, get_or_start_sandbox
|
||||
from services.llm import make_llm_api_call
|
||||
from run_agent_background import run_agent_background, _cleanup_redis_response_list, update_agent_run_status
|
||||
|
@ -71,16 +71,17 @@ class MessageCreateRequest(BaseModel):
|
|||
class AgentCreateRequest(BaseModel):
|
||||
name: str
|
||||
description: Optional[str] = None
|
||||
system_prompt: Optional[str] = None # Make optional to allow defaulting to Suna's system prompt
|
||||
system_prompt: Optional[str] = None
|
||||
configured_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
custom_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
agentpress_tools: Optional[Dict[str, Any]] = {}
|
||||
is_default: Optional[bool] = False
|
||||
# Deprecated, kept for backward-compat
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
# New profile image url (can be external or Supabase storage URL)
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
|
||||
class AgentVersionResponse(BaseModel):
|
||||
version_id: str
|
||||
|
@ -88,7 +89,7 @@ class AgentVersionResponse(BaseModel):
|
|||
version_number: int
|
||||
version_name: str
|
||||
system_prompt: str
|
||||
model: Optional[str] = None # Add model field
|
||||
model: Optional[str] = None
|
||||
configured_mcps: List[Dict[str, Any]]
|
||||
custom_mcps: List[Dict[str, Any]]
|
||||
agentpress_tools: Dict[str, Any]
|
||||
|
@ -102,8 +103,8 @@ class AgentVersionCreateRequest(BaseModel):
|
|||
configured_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
custom_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
agentpress_tools: Optional[Dict[str, Any]] = {}
|
||||
version_name: Optional[str] = None # Custom version name
|
||||
description: Optional[str] = None # Version description
|
||||
version_name: Optional[str] = None
|
||||
description: Optional[str] = None
|
||||
|
||||
class AgentUpdateRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
@ -113,11 +114,12 @@ class AgentUpdateRequest(BaseModel):
|
|||
custom_mcps: Optional[List[Dict[str, Any]]] = None
|
||||
agentpress_tools: Optional[Dict[str, Any]] = None
|
||||
is_default: Optional[bool] = None
|
||||
# Deprecated, kept for backward-compat
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
# New profile image url
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
|
||||
class AgentResponse(BaseModel):
|
||||
agent_id: str
|
||||
|
@ -128,15 +130,15 @@ class AgentResponse(BaseModel):
|
|||
custom_mcps: List[Dict[str, Any]]
|
||||
agentpress_tools: Dict[str, Any]
|
||||
is_default: bool
|
||||
# Deprecated
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
# New
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
created_at: str
|
||||
updated_at: Optional[str] = None
|
||||
is_public: Optional[bool] = False
|
||||
|
||||
tags: Optional[List[str]] = []
|
||||
current_version_id: Optional[str] = None
|
||||
version_count: Optional[int] = 1
|
||||
|
@ -155,7 +157,7 @@ class AgentsResponse(BaseModel):
|
|||
|
||||
class ThreadAgentResponse(BaseModel):
|
||||
agent: Optional[AgentResponse]
|
||||
source: str # "thread", "default", "none", "missing"
|
||||
source: str
|
||||
message: str
|
||||
|
||||
class AgentExportData(BaseModel):
|
||||
|
@ -1544,7 +1546,7 @@ async def get_agents(
|
|||
custom_mcps = agent_config['custom_mcps']
|
||||
agentpress_tools = agent_config['agentpress_tools']
|
||||
|
||||
agent_list.append(AgentResponse(
|
||||
agent_response = AgentResponse(
|
||||
agent_id=agent['agent_id'],
|
||||
name=agent['name'],
|
||||
description=agent.get('description'),
|
||||
|
@ -1558,13 +1560,19 @@ async def get_agents(
|
|||
avatar=agent_config.get('avatar'),
|
||||
avatar_color=agent_config.get('avatar_color'),
|
||||
profile_image_url=agent_config.get('profile_image_url'),
|
||||
icon_name=agent_config.get('icon_name'),
|
||||
icon_color=agent_config.get('icon_color'),
|
||||
icon_background=agent_config.get('icon_background'),
|
||||
created_at=agent['created_at'],
|
||||
updated_at=agent['updated_at'],
|
||||
current_version_id=agent.get('current_version_id'),
|
||||
version_count=agent.get('version_count', 1),
|
||||
current_version=current_version,
|
||||
metadata=agent.get('metadata')
|
||||
))
|
||||
)
|
||||
|
||||
print(f"[DEBUG] get_agents RESPONSE item {agent['name']}: icon_name={agent_response.icon_name}, icon_color={agent_response.icon_color}, icon_background={agent_response.icon_background}")
|
||||
agent_list.append(agent_response)
|
||||
|
||||
total_pages = (total_count + limit - 1) // limit
|
||||
|
||||
|
@ -1593,6 +1601,11 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
)
|
||||
|
||||
logger.debug(f"Fetching agent {agent_id} for user: {user_id}")
|
||||
|
||||
# Debug logging
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] get_agent: Starting to fetch agent {agent_id}")
|
||||
|
||||
client = await db.client
|
||||
|
||||
try:
|
||||
|
@ -1604,6 +1617,11 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
|
||||
agent_data = agent.data[0]
|
||||
|
||||
# Debug logging for fetched agent data
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] get_agent: Fetched agent from DB - icon_name={agent_data.get('icon_name')}, icon_color={agent_data.get('icon_color')}, icon_background={agent_data.get('icon_background')}")
|
||||
print(f"[DEBUG] get_agent: Also has - profile_image_url={agent_data.get('profile_image_url')}, avatar={agent_data.get('avatar')}, avatar_color={agent_data.get('avatar_color')}")
|
||||
|
||||
# Check ownership - only owner can access non-public agents
|
||||
if agent_data['account_id'] != user_id and not agent_data.get('is_public', False):
|
||||
raise HTTPException(status_code=403, detail="Access denied")
|
||||
|
@ -1662,14 +1680,24 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
}
|
||||
|
||||
from agent.config_helper import extract_agent_config
|
||||
|
||||
# Debug logging before extract_agent_config
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] get_agent: Before extract_agent_config - agent_data has icon_name={agent_data.get('icon_name')}, icon_color={agent_data.get('icon_color')}, icon_background={agent_data.get('icon_background')}")
|
||||
|
||||
agent_config = extract_agent_config(agent_data, version_data)
|
||||
|
||||
# Debug logging after extract_agent_config
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] get_agent: After extract_agent_config - agent_config has icon_name={agent_config.get('icon_name')}, icon_color={agent_config.get('icon_color')}, icon_background={agent_config.get('icon_background')}")
|
||||
print(f"[DEBUG] get_agent: Final response will use icon fields from agent_config")
|
||||
|
||||
system_prompt = agent_config['system_prompt']
|
||||
configured_mcps = agent_config['configured_mcps']
|
||||
custom_mcps = agent_config['custom_mcps']
|
||||
agentpress_tools = agent_config['agentpress_tools']
|
||||
|
||||
return AgentResponse(
|
||||
response = AgentResponse(
|
||||
agent_id=agent_data['agent_id'],
|
||||
name=agent_data['name'],
|
||||
description=agent_data.get('description'),
|
||||
|
@ -1683,6 +1711,9 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
avatar=agent_config.get('avatar'),
|
||||
avatar_color=agent_config.get('avatar_color'),
|
||||
profile_image_url=agent_config.get('profile_image_url'),
|
||||
icon_name=agent_config.get('icon_name'),
|
||||
icon_color=agent_config.get('icon_color'),
|
||||
icon_background=agent_config.get('icon_background'),
|
||||
created_at=agent_data['created_at'],
|
||||
updated_at=agent_data.get('updated_at', agent_data['created_at']),
|
||||
current_version_id=agent_data.get('current_version_id'),
|
||||
|
@ -1691,6 +1722,15 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
metadata=agent_data.get('metadata')
|
||||
)
|
||||
|
||||
# Debug logging for the actual response
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] get_agent FINAL RESPONSE: agent_id={response.agent_id}")
|
||||
print(f"[DEBUG] get_agent FINAL RESPONSE: icon_name={response.icon_name}, icon_color={response.icon_color}, icon_background={response.icon_background}")
|
||||
print(f"[DEBUG] get_agent FINAL RESPONSE: profile_image_url={response.profile_image_url}, avatar={response.avatar}, avatar_color={response.avatar_color}")
|
||||
print(f"[DEBUG] get_agent FINAL RESPONSE: Response being sent to frontend with icon fields from agent_config")
|
||||
|
||||
return response
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
@ -1923,15 +1963,19 @@ async def create_agent(
|
|||
"account_id": user_id,
|
||||
"name": agent_data.name,
|
||||
"description": agent_data.description,
|
||||
# Deprecated fields still populated if sent by older clients
|
||||
"avatar": agent_data.avatar,
|
||||
"avatar_color": agent_data.avatar_color,
|
||||
# New profile image url field
|
||||
"profile_image_url": agent_data.profile_image_url,
|
||||
"icon_name": agent_data.icon_name or "bot",
|
||||
"icon_color": agent_data.icon_color or "#000000",
|
||||
"icon_background": agent_data.icon_background or "#F3F4F6",
|
||||
"is_default": agent_data.is_default or False,
|
||||
"version_count": 1
|
||||
}
|
||||
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] create_agent: Creating with icon_name={insert_data.get('icon_name')}, icon_color={insert_data.get('icon_color')}, icon_background={insert_data.get('icon_background')}")
|
||||
|
||||
new_agent = await client.table('agents').insert(insert_data).execute()
|
||||
|
||||
if not new_agent.data:
|
||||
|
@ -1990,7 +2034,8 @@ async def create_agent(
|
|||
await Cache.invalidate(f"agent_count_limit:{user_id}")
|
||||
|
||||
logger.debug(f"Created agent {agent['agent_id']} with v1 for user: {user_id}")
|
||||
return AgentResponse(
|
||||
|
||||
response = AgentResponse(
|
||||
agent_id=agent['agent_id'],
|
||||
name=agent['name'],
|
||||
description=agent.get('description'),
|
||||
|
@ -2005,6 +2050,9 @@ async def create_agent(
|
|||
avatar=agent.get('avatar'),
|
||||
avatar_color=agent.get('avatar_color'),
|
||||
profile_image_url=agent.get('profile_image_url'),
|
||||
icon_name=agent.get('icon_name'),
|
||||
icon_color=agent.get('icon_color'),
|
||||
icon_background=agent.get('icon_background'),
|
||||
created_at=agent['created_at'],
|
||||
updated_at=agent.get('updated_at', agent['created_at']),
|
||||
current_version_id=agent.get('current_version_id'),
|
||||
|
@ -2013,6 +2061,12 @@ async def create_agent(
|
|||
metadata=agent.get('metadata')
|
||||
)
|
||||
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] create_agent RESPONSE: Returning icon_name={response.icon_name}, icon_color={response.icon_color}, icon_background={response.icon_background}")
|
||||
print(f"[DEBUG] create_agent RESPONSE: Also returning profile_image_url={response.profile_image_url}, avatar={response.avatar}, avatar_color={response.avatar_color}")
|
||||
|
||||
return response
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
@ -2053,6 +2107,12 @@ async def update_agent(
|
|||
detail="Custom agent currently disabled. This feature is not available at the moment."
|
||||
)
|
||||
logger.debug(f"Updating agent {agent_id} for user: {user_id}")
|
||||
|
||||
# Debug logging for icon fields
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] update_agent: Received icon fields - icon_name={agent_data.icon_name}, icon_color={agent_data.icon_color}, icon_background={agent_data.icon_background}")
|
||||
print(f"[DEBUG] update_agent: Also received - profile_image_url={agent_data.profile_image_url}, avatar={agent_data.avatar}, avatar_color={agent_data.avatar_color}")
|
||||
|
||||
client = await db.client
|
||||
|
||||
try:
|
||||
|
@ -2252,6 +2312,17 @@ async def update_agent(
|
|||
update_data["avatar_color"] = agent_data.avatar_color
|
||||
if agent_data.profile_image_url is not None:
|
||||
update_data["profile_image_url"] = agent_data.profile_image_url
|
||||
# Handle new icon system fields
|
||||
if agent_data.icon_name is not None:
|
||||
update_data["icon_name"] = agent_data.icon_name
|
||||
if agent_data.icon_color is not None:
|
||||
update_data["icon_color"] = agent_data.icon_color
|
||||
if agent_data.icon_background is not None:
|
||||
update_data["icon_background"] = agent_data.icon_background
|
||||
|
||||
# Debug logging for update_data
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] update_agent: Prepared update_data with icon fields - icon_name={update_data.get('icon_name')}, icon_color={update_data.get('icon_color')}, icon_background={update_data.get('icon_background')}")
|
||||
|
||||
current_system_prompt = agent_data.system_prompt if agent_data.system_prompt is not None else current_version_data.get('system_prompt', '')
|
||||
current_configured_mcps = agent_data.configured_mcps if agent_data.configured_mcps is not None else current_version_data.get('configured_mcps', [])
|
||||
|
@ -2296,12 +2367,24 @@ async def update_agent(
|
|||
|
||||
if update_data:
|
||||
try:
|
||||
print(f"[DEBUG] update_agent DB UPDATE: About to update agent {agent_id} with data: {update_data}")
|
||||
|
||||
update_result = await client.table('agents').update(update_data).eq("agent_id", agent_id).eq("account_id", user_id).execute()
|
||||
|
||||
# Debug logging after DB update
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
if update_result.data:
|
||||
print(f"[DEBUG] update_agent DB UPDATE SUCCESS: Updated {len(update_result.data)} row(s)")
|
||||
print(f"[DEBUG] update_agent DB UPDATE RESULT: {update_result.data[0] if update_result.data else 'No data'}")
|
||||
else:
|
||||
print(f"[DEBUG] update_agent DB UPDATE FAILED: No rows affected")
|
||||
|
||||
if not update_result.data:
|
||||
raise HTTPException(status_code=500, detail="Failed to update agent - no rows affected")
|
||||
except Exception as e:
|
||||
logger.error(f"Error updating agent {agent_id}: {str(e)}")
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] update_agent DB UPDATE ERROR: {str(e)}")
|
||||
raise HTTPException(status_code=500, detail=f"Failed to update agent: {str(e)}")
|
||||
|
||||
updated_agent = await client.table('agents').select('*').eq("agent_id", agent_id).eq("account_id", user_id).maybe_single().execute()
|
||||
|
@ -2311,6 +2394,11 @@ async def update_agent(
|
|||
|
||||
agent = updated_agent.data
|
||||
|
||||
print(f"[DEBUG] update_agent AFTER UPDATE FETCH: agent_id={agent.get('agent_id')}")
|
||||
print(f"[DEBUG] update_agent AFTER UPDATE FETCH: icon_name={agent.get('icon_name')}, icon_color={agent.get('icon_color')}, icon_background={agent.get('icon_background')}")
|
||||
print(f"[DEBUG] update_agent AFTER UPDATE FETCH: profile_image_url={agent.get('profile_image_url')}, avatar={agent.get('avatar')}, avatar_color={agent.get('avatar_color')}")
|
||||
print(f"[DEBUG] update_agent AFTER UPDATE FETCH: All keys in agent: {agent.keys()}")
|
||||
|
||||
current_version = None
|
||||
if agent.get('current_version_id'):
|
||||
try:
|
||||
|
@ -2359,14 +2447,23 @@ async def update_agent(
|
|||
}
|
||||
|
||||
from agent.config_helper import extract_agent_config
|
||||
|
||||
# Debug logging before extract_agent_config
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] update_agent: Before extract_agent_config - agent has icon_name={agent.get('icon_name')}, icon_color={agent.get('icon_color')}, icon_background={agent.get('icon_background')}")
|
||||
|
||||
agent_config = extract_agent_config(agent, version_data)
|
||||
|
||||
# Debug logging after extract_agent_config
|
||||
if config.ENV_MODE == EnvMode.STAGING:
|
||||
print(f"[DEBUG] update_agent: After extract_agent_config - agent_config has icon_name={agent_config.get('icon_name')}, icon_color={agent_config.get('icon_color')}, icon_background={agent_config.get('icon_background')}")
|
||||
|
||||
system_prompt = agent_config['system_prompt']
|
||||
configured_mcps = agent_config['configured_mcps']
|
||||
custom_mcps = agent_config['custom_mcps']
|
||||
agentpress_tools = agent_config['agentpress_tools']
|
||||
|
||||
return AgentResponse(
|
||||
response = AgentResponse(
|
||||
agent_id=agent['agent_id'],
|
||||
name=agent['name'],
|
||||
description=agent.get('description'),
|
||||
|
@ -2380,6 +2477,9 @@ async def update_agent(
|
|||
avatar=agent_config.get('avatar'),
|
||||
avatar_color=agent_config.get('avatar_color'),
|
||||
profile_image_url=agent_config.get('profile_image_url'),
|
||||
icon_name=agent_config.get('icon_name'),
|
||||
icon_color=agent_config.get('icon_color'),
|
||||
icon_background=agent_config.get('icon_background'),
|
||||
created_at=agent['created_at'],
|
||||
updated_at=agent.get('updated_at', agent['created_at']),
|
||||
current_version_id=agent.get('current_version_id'),
|
||||
|
@ -2388,6 +2488,14 @@ async def update_agent(
|
|||
metadata=agent.get('metadata')
|
||||
)
|
||||
|
||||
|
||||
print(f"[DEBUG] update_agent FINAL RESPONSE: agent_id={response.agent_id}")
|
||||
print(f"[DEBUG] update_agent FINAL RESPONSE: icon_name={response.icon_name}, icon_color={response.icon_color}, icon_background={response.icon_background}")
|
||||
print(f"[DEBUG] update_agent FINAL RESPONSE: profile_image_url={response.profile_image_url}, avatar={response.avatar}, avatar_color={response.avatar_color}")
|
||||
print(f"[DEBUG] update_agent FINAL RESPONSE: Full response dict keys: {response.dict().keys()}")
|
||||
|
||||
return response
|
||||
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
from typing import Dict, Any, Optional, List
|
||||
from utils.logger import logger
|
||||
import os
|
||||
|
||||
|
||||
def extract_agent_config(agent_data: Dict[str, Any], version_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
|
@ -8,6 +9,11 @@ def extract_agent_config(agent_data: Dict[str, Any], version_data: Optional[Dict
|
|||
metadata = agent_data.get('metadata', {})
|
||||
is_suna_default = metadata.get('is_suna_default', False)
|
||||
|
||||
# Debug logging
|
||||
if os.getenv("ENV_MODE", "").upper() == "STAGING":
|
||||
print(f"[DEBUG] extract_agent_config: Called for agent {agent_id}, is_suna_default={is_suna_default}")
|
||||
print(f"[DEBUG] extract_agent_config: Input agent_data has icon_name={agent_data.get('icon_name')}, icon_color={agent_data.get('icon_color')}, icon_background={agent_data.get('icon_background')}")
|
||||
|
||||
# Handle Suna agents with special logic
|
||||
if is_suna_default:
|
||||
return _extract_suna_agent_config(agent_data, version_data)
|
||||
|
@ -49,9 +55,7 @@ def _extract_suna_agent_config(agent_data: Dict[str, Any], version_data: Optiona
|
|||
}
|
||||
}
|
||||
|
||||
# Add user customizations from version or agent data
|
||||
if version_data:
|
||||
# Get customizations from version data
|
||||
if version_data.get('config'):
|
||||
version_config = version_data['config']
|
||||
tools = version_config.get('tools', {})
|
||||
|
@ -60,13 +64,11 @@ def _extract_suna_agent_config(agent_data: Dict[str, Any], version_data: Optiona
|
|||
config['workflows'] = version_config.get('workflows', [])
|
||||
config['triggers'] = version_config.get('triggers', [])
|
||||
else:
|
||||
# Legacy version format
|
||||
config['configured_mcps'] = version_data.get('configured_mcps', [])
|
||||
config['custom_mcps'] = version_data.get('custom_mcps', [])
|
||||
config['workflows'] = []
|
||||
config['triggers'] = []
|
||||
else:
|
||||
# Fallback to agent data or empty
|
||||
config['configured_mcps'] = agent_data.get('configured_mcps', [])
|
||||
config['custom_mcps'] = agent_data.get('custom_mcps', [])
|
||||
config['workflows'] = []
|
||||
|
@ -76,13 +78,15 @@ def _extract_suna_agent_config(agent_data: Dict[str, Any], version_data: Optiona
|
|||
|
||||
|
||||
def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optional[Dict[str, Any]] = None) -> Dict[str, Any]:
|
||||
"""Extract config for custom agents using versioning system."""
|
||||
agent_id = agent_data.get('agent_id', 'Unknown')
|
||||
|
||||
# Debug logging for icon fields
|
||||
if os.getenv("ENV_MODE", "").upper() == "STAGING":
|
||||
print(f"[DEBUG] _extract_custom_agent_config: Input agent_data has icon_name={agent_data.get('icon_name')}, icon_color={agent_data.get('icon_color')}, icon_background={agent_data.get('icon_background')}")
|
||||
|
||||
if version_data:
|
||||
logger.debug(f"Using version data for custom agent {agent_id} (version: {version_data.get('version_name', 'unknown')})")
|
||||
|
||||
# Extract from version data
|
||||
if version_data.get('config'):
|
||||
config = version_data['config'].copy()
|
||||
system_prompt = config.get('system_prompt', '')
|
||||
|
@ -94,7 +98,6 @@ def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optio
|
|||
workflows = config.get('workflows', [])
|
||||
triggers = config.get('triggers', [])
|
||||
else:
|
||||
# Legacy version format
|
||||
system_prompt = version_data.get('system_prompt', '')
|
||||
model = version_data.get('model')
|
||||
configured_mcps = version_data.get('configured_mcps', [])
|
||||
|
@ -103,7 +106,7 @@ def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optio
|
|||
workflows = []
|
||||
triggers = []
|
||||
|
||||
return {
|
||||
config = {
|
||||
'agent_id': agent_data['agent_id'],
|
||||
'name': agent_data['name'],
|
||||
'description': agent_data.get('description'),
|
||||
|
@ -117,6 +120,9 @@ def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optio
|
|||
'avatar': agent_data.get('avatar'),
|
||||
'avatar_color': agent_data.get('avatar_color'),
|
||||
'profile_image_url': agent_data.get('profile_image_url'),
|
||||
'icon_name': agent_data.get('icon_name'),
|
||||
'icon_color': agent_data.get('icon_color'),
|
||||
'icon_background': agent_data.get('icon_background'),
|
||||
'is_default': agent_data.get('is_default', False),
|
||||
'is_suna_default': False,
|
||||
'centrally_managed': False,
|
||||
|
@ -125,11 +131,16 @@ def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optio
|
|||
'version_name': version_data.get('version_name', 'v1'),
|
||||
'restrictions': {}
|
||||
}
|
||||
|
||||
# Debug logging for returned config
|
||||
if os.getenv("ENV_MODE", "").upper() == "STAGING":
|
||||
print(f"[DEBUG] _extract_custom_agent_config: Returning config with icon_name={config.get('icon_name')}, icon_color={config.get('icon_color')}, icon_background={config.get('icon_background')}")
|
||||
|
||||
return config
|
||||
|
||||
# Fallback: create default config for custom agents without version data
|
||||
logger.warning(f"No version data found for custom agent {agent_id}, creating default configuration")
|
||||
|
||||
return {
|
||||
fallback_config = {
|
||||
'agent_id': agent_data['agent_id'],
|
||||
'name': agent_data.get('name', 'Unnamed Agent'),
|
||||
'description': agent_data.get('description', ''),
|
||||
|
@ -143,6 +154,9 @@ def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optio
|
|||
'avatar': agent_data.get('avatar'),
|
||||
'avatar_color': agent_data.get('avatar_color'),
|
||||
'profile_image_url': agent_data.get('profile_image_url'),
|
||||
'icon_name': agent_data.get('icon_name'),
|
||||
'icon_color': agent_data.get('icon_color'),
|
||||
'icon_background': agent_data.get('icon_background'),
|
||||
'is_default': agent_data.get('is_default', False),
|
||||
'is_suna_default': False,
|
||||
'centrally_managed': False,
|
||||
|
@ -151,6 +165,12 @@ def _extract_custom_agent_config(agent_data: Dict[str, Any], version_data: Optio
|
|||
'version_name': 'v1',
|
||||
'restrictions': {}
|
||||
}
|
||||
|
||||
# Debug logging for fallback config
|
||||
if os.getenv("ENV_MODE", "").upper() == "STAGING":
|
||||
print(f"[DEBUG] _extract_custom_agent_config: Fallback config with icon_name={fallback_config.get('icon_name')}, icon_color={fallback_config.get('icon_color')}, icon_background={fallback_config.get('icon_background')}")
|
||||
|
||||
return fallback_config
|
||||
|
||||
|
||||
def build_unified_config(
|
||||
|
|
|
@ -54,11 +54,12 @@ class JsonImportService:
|
|||
agent_info = {
|
||||
'name': json_data.get('name', 'Imported Agent'),
|
||||
'description': json_data.get('description', ''),
|
||||
# Deprecated fields
|
||||
'avatar': json_data.get('avatar'),
|
||||
'avatar_color': json_data.get('avatar_color'),
|
||||
# New field
|
||||
'profile_image_url': json_data.get('profile_image_url') or json_data.get('metadata', {}).get('profile_image_url')
|
||||
'profile_image_url': json_data.get('profile_image_url') or json_data.get('metadata', {}).get('profile_image_url'),
|
||||
'icon_name': json_data.get('icon_name', 'brain'),
|
||||
'icon_color': json_data.get('icon_color', '#000000'),
|
||||
'icon_background': json_data.get('icon_background', '#F3F4F6')
|
||||
}
|
||||
|
||||
return JsonImportAnalysis(
|
||||
|
@ -93,11 +94,12 @@ class JsonImportService:
|
|||
agent_info={
|
||||
'name': json_data.get('name', 'Imported Agent'),
|
||||
'description': json_data.get('description', ''),
|
||||
# Deprecated
|
||||
'avatar': json_data.get('avatar'),
|
||||
'avatar_color': json_data.get('avatar_color'),
|
||||
# New
|
||||
'profile_image_url': json_data.get('profile_image_url') or json_data.get('metadata', {}).get('profile_image_url')
|
||||
'profile_image_url': json_data.get('profile_image_url') or json_data.get('metadata', {}).get('profile_image_url'),
|
||||
'icon_name': json_data.get('icon_name', 'brain'),
|
||||
'icon_color': json_data.get('icon_color', '#000000'),
|
||||
'icon_background': json_data.get('icon_background', '#F3F4F6')
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -299,6 +301,10 @@ class JsonImportService:
|
|||
"description": json_data.get('description', ''),
|
||||
"avatar": json_data.get('avatar'),
|
||||
"avatar_color": json_data.get('avatar_color'),
|
||||
"profile_image_url": json_data.get('profile_image_url'),
|
||||
"icon_name": json_data.get('icon_name', 'brain'),
|
||||
"icon_color": json_data.get('icon_color', '#000000'),
|
||||
"icon_background": json_data.get('icon_background', '#F3F4F6'),
|
||||
"is_default": False,
|
||||
"tags": json_data.get('tags', []),
|
||||
"version_count": 1,
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
BEGIN;
|
||||
|
||||
ALTER TABLE agents
|
||||
ADD COLUMN IF NOT EXISTS icon_name VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS icon_color VARCHAR(7) DEFAULT '#000000',
|
||||
ADD COLUMN IF NOT EXISTS icon_background VARCHAR(7) DEFAULT '#F3F4F6';
|
||||
|
||||
ALTER TABLE agent_templates
|
||||
ADD COLUMN IF NOT EXISTS icon_name VARCHAR(100),
|
||||
ADD COLUMN IF NOT EXISTS icon_color VARCHAR(7) DEFAULT '#000000',
|
||||
ADD COLUMN IF NOT EXISTS icon_background VARCHAR(7) DEFAULT '#F3F4F6';
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agents_icon_name ON agents(icon_name) WHERE icon_name IS NOT NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_agent_templates_icon_name ON agent_templates(icon_name) WHERE icon_name IS NOT NULL;
|
||||
|
||||
COMMENT ON COLUMN agents.icon_name IS 'Optional: Lucide React icon name for agent profile (used when profile_image_url is not set)';
|
||||
COMMENT ON COLUMN agents.icon_color IS 'Optional: Hex color for the icon (e.g., #000000)';
|
||||
COMMENT ON COLUMN agents.icon_background IS 'Optional: Hex background color for the icon container';
|
||||
|
||||
COMMENT ON COLUMN agent_templates.icon_name IS 'Optional: Lucide React icon name for template profile (used when profile_image_url is not set)';
|
||||
COMMENT ON COLUMN agent_templates.icon_color IS 'Optional: Hex color for the icon (e.g., #000000)';
|
||||
COMMENT ON COLUMN agent_templates.icon_background IS 'Optional: Hex background color for the icon container';
|
||||
|
||||
COMMIT;
|
|
@ -63,6 +63,9 @@ class TemplateResponse(BaseModel):
|
|||
avatar: Optional[str]
|
||||
avatar_color: Optional[str]
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
metadata: Dict[str, Any]
|
||||
creator_name: Optional[str] = None
|
||||
|
||||
|
|
|
@ -359,6 +359,9 @@ class InstallationService:
|
|||
'avatar': template.avatar,
|
||||
'avatar_color': template.avatar_color,
|
||||
'profile_image_url': template.profile_image_url,
|
||||
'icon_name': template.icon_name or 'brain',
|
||||
'icon_color': template.icon_color or '#000000',
|
||||
'icon_background': template.icon_background or '#F3F4F6',
|
||||
'metadata': {
|
||||
**template.metadata,
|
||||
'created_from_template': template.template_id,
|
||||
|
|
|
@ -49,6 +49,9 @@ class AgentTemplate:
|
|||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
metadata: ConfigType = field(default_factory=dict)
|
||||
creator_name: Optional[str] = None
|
||||
|
||||
|
@ -223,6 +226,9 @@ class TemplateService:
|
|||
avatar=agent.get('avatar'),
|
||||
avatar_color=agent.get('avatar_color'),
|
||||
profile_image_url=agent.get('profile_image_url'),
|
||||
icon_name=agent.get('icon_name'),
|
||||
icon_color=agent.get('icon_color'),
|
||||
icon_background=agent.get('icon_background'),
|
||||
metadata=agent.get('metadata', {})
|
||||
)
|
||||
|
||||
|
@ -629,6 +635,9 @@ class TemplateService:
|
|||
'avatar': template.avatar,
|
||||
'avatar_color': template.avatar_color,
|
||||
'profile_image_url': template.profile_image_url,
|
||||
'icon_name': template.icon_name,
|
||||
'icon_color': template.icon_color,
|
||||
'icon_background': template.icon_background,
|
||||
'metadata': template.metadata
|
||||
}
|
||||
|
||||
|
@ -653,6 +662,9 @@ class TemplateService:
|
|||
avatar=data.get('avatar'),
|
||||
avatar_color=data.get('avatar_color'),
|
||||
profile_image_url=data.get('profile_image_url'),
|
||||
icon_name=data.get('icon_name'),
|
||||
icon_color=data.get('icon_color'),
|
||||
icon_background=data.get('icon_background'),
|
||||
metadata=data.get('metadata', {}),
|
||||
creator_name=creator_name
|
||||
)
|
||||
|
|
|
@ -136,6 +136,9 @@ def format_template_for_response(template: AgentTemplate) -> Dict[str, Any]:
|
|||
'avatar': template.avatar,
|
||||
'avatar_color': template.avatar_color,
|
||||
'profile_image_url': template.profile_image_url,
|
||||
'icon_name': template.icon_name,
|
||||
'icon_color': template.icon_color,
|
||||
'icon_background': template.icon_background,
|
||||
'metadata': template.metadata,
|
||||
'creator_name': template.creator_name
|
||||
}
|
||||
|
|
|
@ -101,6 +101,9 @@ interface FormData {
|
|||
custom_mcps: any[];
|
||||
is_default: boolean;
|
||||
profile_image_url?: string;
|
||||
icon_name?: string | null;
|
||||
icon_color: string;
|
||||
icon_background: string;
|
||||
}
|
||||
|
||||
function AgentConfigurationContent() {
|
||||
|
@ -130,6 +133,9 @@ function AgentConfigurationContent() {
|
|||
custom_mcps: [],
|
||||
is_default: false,
|
||||
profile_image_url: '',
|
||||
icon_name: null,
|
||||
icon_color: '#000000',
|
||||
icon_background: '#e5e5e5',
|
||||
});
|
||||
|
||||
const [originalData, setOriginalData] = useState<FormData>(formData);
|
||||
|
@ -147,6 +153,9 @@ function AgentConfigurationContent() {
|
|||
configured_mcps: versionData.configured_mcps,
|
||||
custom_mcps: versionData.custom_mcps,
|
||||
agentpress_tools: versionData.agentpress_tools,
|
||||
icon_name: versionData.icon_name || agent.icon_name,
|
||||
icon_color: versionData.icon_color || agent.icon_color,
|
||||
icon_background: versionData.icon_background || agent.icon_background,
|
||||
};
|
||||
}
|
||||
const newFormData: FormData = {
|
||||
|
@ -159,6 +168,9 @@ function AgentConfigurationContent() {
|
|||
custom_mcps: configSource.custom_mcps || [],
|
||||
is_default: configSource.is_default || false,
|
||||
profile_image_url: configSource.profile_image_url || '',
|
||||
icon_name: configSource.icon_name || null,
|
||||
icon_color: configSource.icon_color || '#000000',
|
||||
icon_background: configSource.icon_background || '#e5e5e5',
|
||||
};
|
||||
setFormData(newFormData);
|
||||
setOriginalData(newFormData);
|
||||
|
@ -174,6 +186,9 @@ function AgentConfigurationContent() {
|
|||
custom_mcps: versionData.custom_mcps || formData.custom_mcps,
|
||||
is_default: formData.is_default,
|
||||
profile_image_url: formData.profile_image_url,
|
||||
icon_name: versionData.icon_name || formData.icon_name || null,
|
||||
icon_color: versionData.icon_color || formData.icon_color || '#000000',
|
||||
icon_background: versionData.icon_background || formData.icon_background || '#e5e5e5',
|
||||
} : formData;
|
||||
|
||||
const handleFieldChange = useCallback((field: string, value: any) => {
|
||||
|
@ -235,6 +250,9 @@ function AgentConfigurationContent() {
|
|||
description: formData.description,
|
||||
is_default: formData.is_default,
|
||||
profile_image_url: formData.profile_image_url,
|
||||
icon_name: formData.icon_name,
|
||||
icon_color: formData.icon_color,
|
||||
icon_background: formData.icon_background,
|
||||
system_prompt: formData.system_prompt,
|
||||
agentpress_tools: formData.agentpress_tools,
|
||||
configured_mcps: formData.configured_mcps,
|
||||
|
@ -275,12 +293,39 @@ function AgentConfigurationContent() {
|
|||
|
||||
setFormData(prev => ({ ...prev, profile_image_url: profileImageUrl || '' }));
|
||||
setOriginalData(prev => ({ ...prev, profile_image_url: profileImageUrl || '' }));
|
||||
toast.success('Profile picture updated');
|
||||
} catch (error) {
|
||||
toast.error('Failed to update profile picture');
|
||||
throw error;
|
||||
}
|
||||
}, [agentId, updateAgentMutation]);
|
||||
|
||||
const handleIconSave = useCallback(async (iconName: string | null, iconColor: string, iconBackground: string) => {
|
||||
try {
|
||||
await updateAgentMutation.mutateAsync({
|
||||
agentId,
|
||||
icon_name: iconName,
|
||||
icon_color: iconColor,
|
||||
icon_background: iconBackground,
|
||||
});
|
||||
|
||||
setFormData(prev => ({
|
||||
...prev,
|
||||
icon_name: iconName,
|
||||
icon_color: iconColor,
|
||||
icon_background: iconBackground,
|
||||
}));
|
||||
setOriginalData(prev => ({
|
||||
...prev,
|
||||
icon_name: iconName,
|
||||
icon_color: iconColor,
|
||||
icon_background: iconBackground,
|
||||
}));
|
||||
toast.success('Agent icon updated');
|
||||
} catch (error) {
|
||||
toast.error('Failed to update agent icon');
|
||||
throw error;
|
||||
}
|
||||
}, [agentId, updateAgentMutation]);
|
||||
|
||||
const handleSystemPromptSave = useCallback(async (value: string) => {
|
||||
try {
|
||||
|
@ -300,8 +345,6 @@ function AgentConfigurationContent() {
|
|||
|
||||
const handleModelSave = useCallback(async (model: string) => {
|
||||
try {
|
||||
// Model updates might need to be handled through a different endpoint
|
||||
// For now, just update the local state
|
||||
setFormData(prev => ({ ...prev, model }));
|
||||
setOriginalData(prev => ({ ...prev, model }));
|
||||
toast.success('Model updated');
|
||||
|
@ -403,6 +446,7 @@ function AgentConfigurationContent() {
|
|||
}}
|
||||
onNameSave={handleNameSave}
|
||||
onProfileImageSave={handleProfileImageSave}
|
||||
onIconSave={handleIconSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -471,6 +515,7 @@ function AgentConfigurationContent() {
|
|||
}}
|
||||
onNameSave={handleNameSave}
|
||||
onProfileImageSave={handleProfileImageSave}
|
||||
onIconSave={handleIconSave}
|
||||
/>
|
||||
<Drawer>
|
||||
<DrawerTrigger asChild>
|
||||
|
|
|
@ -171,13 +171,17 @@ export default function AgentsPage() {
|
|||
created_at: template.created_at,
|
||||
marketplace_published_at: template.marketplace_published_at,
|
||||
profile_image_url: template.profile_image_url,
|
||||
avatar: template.avatar,
|
||||
avatar_color: template.avatar_color,
|
||||
icon_name: template.icon_name,
|
||||
icon_color: template.icon_color,
|
||||
icon_background: template.icon_background,
|
||||
template_id: template.template_id,
|
||||
is_kortix_team: template.is_kortix_team,
|
||||
mcp_requirements: template.mcp_requirements,
|
||||
metadata: template.metadata,
|
||||
};
|
||||
|
||||
// Backend handles search filtering, so we just transform the data
|
||||
allItems.push(item);
|
||||
|
||||
if (user?.id === template.creator_id) {
|
||||
|
|
|
@ -51,6 +51,10 @@ interface ApiMessageType extends BaseApiMessageType {
|
|||
name: string;
|
||||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
profile_image_url?: string;
|
||||
icon_name?: string;
|
||||
icon_color?: string;
|
||||
icon_background?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -39,6 +39,7 @@ import Image from 'next/image';
|
|||
import { useTheme } from 'next-themes';
|
||||
import ColorThief from 'colorthief';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
interface MarketplaceTemplate {
|
||||
template_id: string;
|
||||
|
@ -58,6 +59,9 @@ interface MarketplaceTemplate {
|
|||
avatar: string | null;
|
||||
avatar_color: string | null;
|
||||
profile_image_url: string | null;
|
||||
icon_name: string | null;
|
||||
icon_color: string | null;
|
||||
icon_background: string | null;
|
||||
metadata: Record<string, any>;
|
||||
creator_name: string | null;
|
||||
}
|
||||
|
@ -256,7 +260,20 @@ export default function TemplateSharePage() {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (template?.profile_image_url && imageRef.current && imageLoaded) {
|
||||
if (template?.icon_name && template?.icon_background) {
|
||||
// For icons, use the icon background color as the primary color
|
||||
const iconBg = template.icon_background || '#e5e5e5';
|
||||
const iconColor = template.icon_color || '#000000';
|
||||
// Create a palette based on the icon colors
|
||||
setColorPalette([
|
||||
iconBg,
|
||||
iconColor,
|
||||
'#6366f1',
|
||||
'#8b5cf6',
|
||||
'#ec4899',
|
||||
'#f43f5e'
|
||||
]);
|
||||
} else if (template?.profile_image_url && imageRef.current && imageLoaded) {
|
||||
const colorThief = new ColorThief();
|
||||
try {
|
||||
const palette = colorThief.getPalette(imageRef.current, 6);
|
||||
|
@ -270,13 +287,13 @@ export default function TemplateSharePage() {
|
|||
'#f43f5e', '#f97316', '#facc15'
|
||||
]);
|
||||
}
|
||||
} else if (!template?.profile_image_url) {
|
||||
} else {
|
||||
setColorPalette([
|
||||
'#6366f1', '#8b5cf6', '#ec4899',
|
||||
'#f43f5e', '#f97316', '#facc15'
|
||||
]);
|
||||
}
|
||||
}, [template?.profile_image_url, imageLoaded]);
|
||||
}, [template?.profile_image_url, template?.icon_name, template?.icon_background, template?.icon_color, imageLoaded]);
|
||||
|
||||
const handleInstall = () => {
|
||||
if (!template) return;
|
||||
|
@ -450,7 +467,18 @@ export default function TemplateSharePage() {
|
|||
/>
|
||||
)}
|
||||
<div className="relative aspect-square w-full max-w-sm mx-auto lg:mx-0 rounded-2xl overflow-hidden bg-background">
|
||||
{template.profile_image_url ? (
|
||||
{template.icon_name ? (
|
||||
<div
|
||||
className="w-full h-full flex items-center justify-center"
|
||||
style={{ backgroundColor: template.icon_background || '#e5e5e5' }}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={template.icon_name as any}
|
||||
size={120}
|
||||
color={template.icon_color || '#000000'}
|
||||
/>
|
||||
</div>
|
||||
) : template.profile_image_url ? (
|
||||
<>
|
||||
<img
|
||||
ref={imageRef}
|
||||
|
|
|
@ -15,6 +15,7 @@ import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query
|
|||
import { BillingError } from '@/lib/api';
|
||||
import { normalizeFilenameToNFC } from '@/lib/utils/unicode';
|
||||
import { KortixLogo } from '../sidebar/kortix-logo';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
interface Agent {
|
||||
agent_id: string;
|
||||
|
@ -27,6 +28,10 @@ interface Agent {
|
|||
created_at?: string;
|
||||
updated_at?: string;
|
||||
profile_image_url?: string;
|
||||
// Icon system fields
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
}
|
||||
|
||||
interface AgentPreviewProps {
|
||||
|
@ -63,6 +68,20 @@ export const AgentPreview = ({ agent, agentMetadata }: AgentPreviewProps) => {
|
|||
if (isSunaAgent) {
|
||||
return <KortixLogo size={16} />;
|
||||
}
|
||||
if (agent.icon_name) {
|
||||
return (
|
||||
<div
|
||||
className="h-4 w-4 flex items-center justify-center rounded-sm"
|
||||
style={{ backgroundColor: agent.icon_background || '#F3F4F6' }}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={agent.icon_name as any}
|
||||
size={14}
|
||||
color={agent.icon_color || '#000000'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
if (agent.profile_image_url) {
|
||||
return (
|
||||
<img
|
||||
|
@ -76,7 +95,7 @@ export const AgentPreview = ({ agent, agentMetadata }: AgentPreviewProps) => {
|
|||
return <div className="text-base leading-none">{avatar}</div>;
|
||||
}
|
||||
return <KortixLogo size={16} />;
|
||||
}, [agent.profile_image_url, agent.name, avatar, isSunaAgent]);
|
||||
}, [agent.profile_image_url, agent.icon_name, agent.icon_color, agent.icon_background, agent.name, avatar, isSunaAgent]);
|
||||
|
||||
const initiateAgentMutation = useInitiateAgentWithInvalidation();
|
||||
const addUserMessageMutation = useAddUserMessageMutation();
|
||||
|
@ -349,9 +368,22 @@ export const AgentPreview = ({ agent, agentMetadata }: AgentPreviewProps) => {
|
|||
agentData={agent}
|
||||
emptyStateComponent={
|
||||
<div className="flex flex-col items-center text-center text-muted-foreground/80">
|
||||
<div className="flex w-20 aspect-square items-center justify-center rounded-2xl bg-muted-foreground/10 p-4 mb-4">
|
||||
<div className="flex w-20 aspect-square items-center justify-center rounded-2xl bg-muted-foreground/10 mb-4">
|
||||
{isSunaAgent ? (
|
||||
<KortixLogo size={36} />
|
||||
) : agent.icon_name ? (
|
||||
<div
|
||||
className="w-full h-full rounded-3xl flex items-center justify-center"
|
||||
style={{
|
||||
backgroundColor: agent.icon_background || '#e5e5e5'
|
||||
}}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={agent.icon_name as any}
|
||||
size={36}
|
||||
style={{ color: agent.icon_color || '#000000' }}
|
||||
/>
|
||||
</div>
|
||||
) : agent.profile_image_url ? (
|
||||
<img
|
||||
src={agent.profile_image_url}
|
||||
|
|
|
@ -9,6 +9,7 @@ import { useCreateTemplate, useUnpublishTemplate } from '@/hooks/react-query/sec
|
|||
import { toast } from 'sonner';
|
||||
import { AgentCard } from './custom-agents-page/agent-card';
|
||||
import { KortixLogo } from '../sidebar/kortix-logo';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
interface Agent {
|
||||
agent_id: string;
|
||||
|
@ -43,6 +44,10 @@ interface Agent {
|
|||
};
|
||||
};
|
||||
profile_image_url?: string;
|
||||
// Icon system fields
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
}
|
||||
|
||||
interface AgentsGridProps {
|
||||
|
@ -98,7 +103,18 @@ const AgentModal: React.FC<AgentModalProps> = ({
|
|||
<div className="p-6">
|
||||
<KortixLogo size={48} />
|
||||
</div>
|
||||
) : agent.profile_image_url ? (
|
||||
) : agent.icon_name ? (
|
||||
<div
|
||||
className="h-16 w-16 rounded-xl flex items-center justify-center"
|
||||
style={{ backgroundColor: agent.icon_background || '#F3F4F6' }}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={agent.icon_name as any}
|
||||
size={32}
|
||||
color={agent.icon_color || '#000000'}
|
||||
/>
|
||||
</div>
|
||||
) : agent.profile_image_url ? (
|
||||
<img src={agent.profile_image_url} alt={agent.name} className="h-16 w-16 rounded-xl object-cover" />
|
||||
) : (
|
||||
<div className="h-16 w-16 rounded-xl bg-muted flex items-center justify-center">
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
import React, { useState, useRef, KeyboardEvent } from 'react';
|
||||
import { Sparkles, Settings, Download, Image as ImageIcon } from 'lucide-react';
|
||||
import { Tabs, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
'use client';
|
||||
|
||||
import React, { useState, useRef, useEffect } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Download } from 'lucide-react';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import { ProfilePictureDialog } from './profile-picture-dialog';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { AgentIconAvatar } from './agent-icon-avatar';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { ProfilePictureDialog } from './profile-picture-dialog';
|
||||
import { AgentVersionSwitcher } from '../agent-version-switcher';
|
||||
import { UpcomingRunsDropdown } from '../upcoming-runs-dropdown';
|
||||
|
||||
|
@ -19,6 +20,9 @@ interface AgentHeaderProps {
|
|||
name: string;
|
||||
description?: string;
|
||||
profile_image_url?: string;
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
};
|
||||
isViewingOldVersion: boolean;
|
||||
onFieldChange: (field: string, value: any) => void;
|
||||
|
@ -31,7 +35,6 @@ interface AgentHeaderProps {
|
|||
name_editable?: boolean;
|
||||
};
|
||||
};
|
||||
// Version control props
|
||||
currentVersionId?: string;
|
||||
currentFormData?: {
|
||||
system_prompt: string;
|
||||
|
@ -43,6 +46,7 @@ interface AgentHeaderProps {
|
|||
onVersionCreated?: () => void;
|
||||
onNameSave?: (name: string) => Promise<void>;
|
||||
onProfileImageSave?: (profileImageUrl: string | null) => Promise<void>;
|
||||
onIconSave?: (iconName: string | null, iconColor: string, iconBackground: string) => Promise<void>;
|
||||
}
|
||||
|
||||
export function AgentHeader({
|
||||
|
@ -59,6 +63,7 @@ export function AgentHeader({
|
|||
onVersionCreated,
|
||||
onNameSave,
|
||||
onProfileImageSave,
|
||||
onIconSave,
|
||||
}: AgentHeaderProps) {
|
||||
const [isProfileDialogOpen, setIsProfileDialogOpen] = useState(false);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
|
@ -99,7 +104,6 @@ export function AgentHeader({
|
|||
return;
|
||||
}
|
||||
|
||||
// Use dedicated save handler if available, otherwise fallback to generic onFieldChange
|
||||
if (onNameSave) {
|
||||
await onNameSave(editName);
|
||||
} else {
|
||||
|
@ -110,7 +114,7 @@ export function AgentHeader({
|
|||
setIsEditing(false);
|
||||
};
|
||||
|
||||
const handleKeyDown = (e: KeyboardEvent<HTMLInputElement>) => {
|
||||
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||
if (e.key === 'Enter') {
|
||||
saveNewName();
|
||||
} else if (e.key === 'Escape') {
|
||||
|
@ -119,18 +123,30 @@ export function AgentHeader({
|
|||
};
|
||||
|
||||
const handleImageUpdate = (url: string | null) => {
|
||||
// Use dedicated save handler if available, otherwise fallback to generic onFieldChange
|
||||
if (onProfileImageSave) {
|
||||
onProfileImageSave(url);
|
||||
} else {
|
||||
onFieldChange('profile_image_url', url);
|
||||
}
|
||||
};
|
||||
|
||||
const handleIconUpdate = async (iconName: string | null, iconColor: string, backgroundColor: string) => {
|
||||
if (onIconSave) {
|
||||
await onIconSave(iconName, iconColor, backgroundColor);
|
||||
} else {
|
||||
onFieldChange('icon_name', iconName);
|
||||
onFieldChange('icon_color', iconColor);
|
||||
onFieldChange('icon_background', backgroundColor);
|
||||
}
|
||||
|
||||
if (iconName && displayData.profile_image_url) {
|
||||
handleImageUpdate(null);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<header className="bg-background sticky top-0 flex h-14 shrink-0 items-center gap-3 z-20 w-full px-8 mb-2">
|
||||
{/* Left side - Agent info */}
|
||||
<div className="flex items-center gap-3 min-w-0">
|
||||
<div className="relative flex-shrink-0">
|
||||
{isSunaAgent ? (
|
||||
|
@ -143,19 +159,15 @@ export function AgentHeader({
|
|||
onClick={() => setIsProfileDialogOpen(true)}
|
||||
type="button"
|
||||
>
|
||||
<Avatar className="h-9 w-9 rounded-lg ring-1 ring-black/5 hover:ring-black/10 transition-colors">
|
||||
{displayData.profile_image_url ? (
|
||||
<AvatarImage
|
||||
src={displayData.profile_image_url}
|
||||
alt={displayData.name}
|
||||
className="object-cover rounded-lg"
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className="rounded-lg text-xs hover:bg-muted">
|
||||
<ImageIcon className="h-4 w-4" />
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
<AgentIconAvatar
|
||||
profileImageUrl={displayData.profile_image_url}
|
||||
iconName={displayData.icon_name}
|
||||
iconColor={displayData.icon_color}
|
||||
backgroundColor={displayData.icon_background}
|
||||
agentName={displayData.name}
|
||||
size={36}
|
||||
className="ring-1 ring-black/5 hover:ring-black/10 transition-all"
|
||||
/>
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
@ -188,10 +200,7 @@ export function AgentHeader({
|
|||
</div>
|
||||
|
||||
<div className="flex flex-1 items-center gap-2">
|
||||
{/* Spacer to push content to the right */}
|
||||
</div>
|
||||
|
||||
{/* Right side - Version controls, tabs and actions aligned together */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex items-center gap-1">
|
||||
{!isSunaAgent && currentFormData && (
|
||||
|
@ -229,8 +238,12 @@ export function AgentHeader({
|
|||
isOpen={isProfileDialogOpen}
|
||||
onClose={() => setIsProfileDialogOpen(false)}
|
||||
currentImageUrl={displayData.profile_image_url}
|
||||
currentIconName={displayData.icon_name}
|
||||
currentIconColor={displayData.icon_color}
|
||||
currentBackgroundColor={displayData.icon_background}
|
||||
agentName={displayData.name}
|
||||
onImageUpdate={handleImageUpdate}
|
||||
onIconUpdate={handleIconUpdate}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -0,0 +1,112 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface AgentIconAvatarProps {
|
||||
profileImageUrl?: string | null;
|
||||
iconName?: string | null;
|
||||
iconColor?: string;
|
||||
backgroundColor?: string;
|
||||
agentName?: string;
|
||||
size?: number;
|
||||
className?: string;
|
||||
isSunaDefault?: boolean;
|
||||
}
|
||||
|
||||
export function AgentIconAvatar({
|
||||
profileImageUrl,
|
||||
iconName,
|
||||
iconColor = '#000000',
|
||||
backgroundColor = '#F3F4F6',
|
||||
agentName = 'Agent',
|
||||
size = 40,
|
||||
className,
|
||||
isSunaDefault = false
|
||||
}: AgentIconAvatarProps) {
|
||||
if (isSunaDefault) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-lg bg-muted border",
|
||||
className
|
||||
)}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<KortixLogo size={size * 0.6} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (iconName) {
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-lg transition-all",
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor
|
||||
}}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={iconName as any}
|
||||
size={size * 0.5}
|
||||
color={iconColor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (profileImageUrl) {
|
||||
return (
|
||||
<Avatar
|
||||
className={cn("rounded-lg", className)}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<AvatarImage
|
||||
src={profileImageUrl}
|
||||
alt={agentName}
|
||||
className="object-cover"
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
<div className="w-full h-full flex items-center justify-center bg-muted">
|
||||
<DynamicIcon
|
||||
name="bot"
|
||||
size={size * 0.5}
|
||||
color="#6B7280"
|
||||
/>
|
||||
</div>
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={cn(
|
||||
"flex items-center justify-center rounded-lg bg-muted",
|
||||
className
|
||||
)}
|
||||
style={{ width: size, height: size }}
|
||||
>
|
||||
<DynamicIcon
|
||||
name="bot"
|
||||
size={size * 0.5}
|
||||
color="#6B7280"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function hasCustomProfile(agent: {
|
||||
profile_image_url?: string | null;
|
||||
icon_name?: string | null;
|
||||
}): boolean {
|
||||
return !!(agent.icon_name || agent.profile_image_url);
|
||||
}
|
|
@ -0,0 +1,143 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState, useMemo } from 'react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
import { icons } from 'lucide-react';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
interface IconPickerProps {
|
||||
selectedIcon?: string;
|
||||
onIconSelect: (iconName: string) => void;
|
||||
iconColor?: string;
|
||||
backgroundColor?: string;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
function toKebabCase(str: string): string {
|
||||
return str
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1-$2')
|
||||
.replace(/([A-Z])([A-Z][a-z])/g, '$1-$2')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
export function IconPicker({
|
||||
selectedIcon,
|
||||
onIconSelect,
|
||||
iconColor = '#000000',
|
||||
backgroundColor = '#F3F4F6',
|
||||
className
|
||||
}: IconPickerProps) {
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
|
||||
const allIconNames = useMemo(() => {
|
||||
return Object.keys(icons).map(name => toKebabCase(name)).sort();
|
||||
}, []);
|
||||
|
||||
const filteredIcons = useMemo(() => {
|
||||
if (!searchQuery) return allIconNames;
|
||||
|
||||
const query = searchQuery.toLowerCase();
|
||||
return allIconNames.filter(name =>
|
||||
name.includes(query) ||
|
||||
name.replace(/-/g, ' ').includes(query)
|
||||
);
|
||||
}, [allIconNames, searchQuery]);
|
||||
|
||||
const popularIcons = [
|
||||
'bot', 'brain', 'sparkles', 'zap', 'rocket',
|
||||
'briefcase', 'code', 'database', 'globe', 'heart',
|
||||
'lightbulb', 'message-circle', 'shield', 'star', 'user',
|
||||
'cpu', 'terminal', 'settings', 'wand-2', 'layers'
|
||||
];
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col", className)}>
|
||||
<div className="pb-3 shrink-0">
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="Search icons..."
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
className="w-full"
|
||||
/>
|
||||
</div>
|
||||
<ScrollArea className="flex-1 rounded-lg border min-h-0">
|
||||
<div className="p-4 space-y-6">
|
||||
{!searchQuery && (
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
Popular Icons
|
||||
</p>
|
||||
<div className="grid grid-cols-5 sm:grid-cols-8 gap-2">
|
||||
{popularIcons.map((iconName) => (
|
||||
<button
|
||||
key={iconName}
|
||||
onClick={() => onIconSelect(iconName)}
|
||||
className={cn(
|
||||
"p-2 sm:p-3 rounded-md border transition-all hover:scale-105 flex items-center justify-center aspect-square",
|
||||
selectedIcon === iconName
|
||||
? "border-primary bg-primary/10 ring-1 ring-primary/30"
|
||||
: "border-border hover:border-primary/60 hover:bg-accent"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: selectedIcon === iconName ? backgroundColor : undefined
|
||||
}}
|
||||
title={iconName}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={iconName as any}
|
||||
size={18}
|
||||
color={selectedIcon === iconName ? iconColor : undefined}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="space-y-3">
|
||||
<p className="text-xs font-semibold text-muted-foreground uppercase tracking-wider">
|
||||
{searchQuery
|
||||
? `Search Results (${filteredIcons.length})`
|
||||
: `All Icons (${allIconNames.length})`
|
||||
}
|
||||
</p>
|
||||
|
||||
{filteredIcons.length === 0 ? (
|
||||
<div className="text-center py-12 text-sm text-muted-foreground">
|
||||
<p>No icons found matching "{searchQuery}"</p>
|
||||
<p className="text-xs mt-2">Try a different search term</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-4 sm:grid-cols-8 gap-2">
|
||||
{filteredIcons.map((iconName) => (
|
||||
<button
|
||||
key={iconName}
|
||||
onClick={() => onIconSelect(iconName)}
|
||||
className={cn(
|
||||
"rounded-md border transition-all hover:scale-105 flex items-center justify-center aspect-square",
|
||||
selectedIcon === iconName
|
||||
? "border-primary bg-primary/10 ring-1 ring-primary/30"
|
||||
: "border-border hover:border-primary/60 hover:bg-accent"
|
||||
)}
|
||||
style={{
|
||||
backgroundColor: selectedIcon === iconName ? backgroundColor : undefined
|
||||
}}
|
||||
title={iconName}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={iconName as any}
|
||||
size={18}
|
||||
color={selectedIcon === iconName ? iconColor : undefined}
|
||||
/>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -1,22 +1,34 @@
|
|||
'use client';
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { Upload, Link2, X, Image as ImageIcon } from 'lucide-react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import React, { useState, useCallback, useEffect } from 'react';
|
||||
import { Sparkles } from 'lucide-react';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogFooter
|
||||
} from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Avatar, AvatarImage, AvatarFallback } from '@/components/ui/avatar';
|
||||
import { toast } from 'sonner';
|
||||
import { createClient } from '@/lib/supabase/client';
|
||||
import { IconPicker } from './icon-picker';
|
||||
import { AgentIconAvatar } from './agent-icon-avatar';
|
||||
import { Separator } from '@/components/ui/separator';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
|
||||
interface ProfilePictureDialogProps {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
currentImageUrl?: string;
|
||||
agentName: string;
|
||||
agentName?: string;
|
||||
onImageUpdate: (url: string | null) => void;
|
||||
currentIconName?: string;
|
||||
currentIconColor?: string;
|
||||
currentBackgroundColor?: string;
|
||||
onIconUpdate?: (iconName: string | null, iconColor: string, backgroundColor: string) => void;
|
||||
}
|
||||
|
||||
export function ProfilePictureDialog({
|
||||
|
@ -25,272 +37,188 @@ export function ProfilePictureDialog({
|
|||
currentImageUrl,
|
||||
agentName,
|
||||
onImageUpdate,
|
||||
currentIconName,
|
||||
currentIconColor = '#000000',
|
||||
currentBackgroundColor = '#F3F4F6',
|
||||
onIconUpdate,
|
||||
}: ProfilePictureDialogProps) {
|
||||
const [isUploading, setIsUploading] = useState(false);
|
||||
const [isUrlSubmitting, setIsUrlSubmitting] = useState(false);
|
||||
const [customUrl, setCustomUrl] = useState('');
|
||||
const [previewUrl, setPreviewUrl] = useState<string | null>(null);
|
||||
const [dragActive, setDragActive] = useState(false);
|
||||
const [selectedIcon, setSelectedIcon] = useState(currentIconName || 'bot');
|
||||
const [iconColor, setIconColor] = useState(currentIconColor || '#000000');
|
||||
const [backgroundColor, setBackgroundColor] = useState(currentBackgroundColor || '#e5e5e5');
|
||||
|
||||
const handleFileUpload = async (file: File) => {
|
||||
if (!file.type.startsWith('image/')) {
|
||||
toast.error('Please select an image file');
|
||||
return;
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedIcon(currentIconName || 'bot');
|
||||
setIconColor(currentIconColor || '#000000');
|
||||
setBackgroundColor(currentBackgroundColor || '#e5e5e5');
|
||||
}
|
||||
|
||||
if (file.size > 5 * 1024 * 1024) { // 5MB limit
|
||||
toast.error('File size must be less than 5MB');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUploading(true);
|
||||
try {
|
||||
const supabase = createClient();
|
||||
const { data: { session } } = await supabase.auth.getSession();
|
||||
|
||||
if (!session) {
|
||||
toast.error('You must be logged in to upload images');
|
||||
return;
|
||||
}
|
||||
|
||||
const form = new FormData();
|
||||
form.append('file', file);
|
||||
|
||||
const res = await fetch(`${process.env.NEXT_PUBLIC_BACKEND_URL}/agents/profile-image/upload`, {
|
||||
method: 'POST',
|
||||
body: form,
|
||||
headers: {
|
||||
'Authorization': `Bearer ${session.access_token}`,
|
||||
}
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const error = await res.json().catch(() => ({ message: 'Upload failed' }));
|
||||
throw new Error(error.message || 'Upload failed');
|
||||
}
|
||||
|
||||
const data = await res.json();
|
||||
if (data?.url) {
|
||||
onImageUpdate(data.url);
|
||||
toast.success('Profile image uploaded successfully!');
|
||||
onClose();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Upload error:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Failed to upload image');
|
||||
} finally {
|
||||
setIsUploading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleFileInputChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const file = e.target.files?.[0];
|
||||
if (file) {
|
||||
handleFileUpload(file);
|
||||
}
|
||||
// Reset input
|
||||
e.target.value = '';
|
||||
};
|
||||
|
||||
const handleDrop = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(false);
|
||||
|
||||
const file = e.dataTransfer.files?.[0];
|
||||
if (file) {
|
||||
handleFileUpload(file);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDragOver = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(true);
|
||||
};
|
||||
|
||||
const handleDragLeave = (e: React.DragEvent) => {
|
||||
e.preventDefault();
|
||||
setDragActive(false);
|
||||
};
|
||||
|
||||
const handleUrlSubmit = async () => {
|
||||
if (!customUrl.trim()) {
|
||||
toast.error('Please enter a valid URL');
|
||||
return;
|
||||
}
|
||||
|
||||
// Basic URL validation
|
||||
try {
|
||||
new URL(customUrl);
|
||||
} catch {
|
||||
toast.error('Please enter a valid URL');
|
||||
return;
|
||||
}
|
||||
|
||||
setIsUrlSubmitting(true);
|
||||
try {
|
||||
onImageUpdate(customUrl);
|
||||
toast.success('Profile image URL updated successfully!');
|
||||
}, [isOpen, currentIconName, currentIconColor, currentBackgroundColor]);
|
||||
|
||||
const handleIconSave = useCallback(() => {
|
||||
if (onIconUpdate) {
|
||||
onIconUpdate(selectedIcon, iconColor, backgroundColor);
|
||||
onImageUpdate(null);
|
||||
toast.success('Agent icon updated!');
|
||||
onClose();
|
||||
} catch (err) {
|
||||
toast.error('Failed to update profile image URL');
|
||||
} finally {
|
||||
setIsUrlSubmitting(false);
|
||||
}
|
||||
};
|
||||
}, [selectedIcon, iconColor, backgroundColor, onIconUpdate, onImageUpdate, onClose]);
|
||||
|
||||
const handleUrlPreview = () => {
|
||||
if (customUrl) {
|
||||
try {
|
||||
new URL(customUrl);
|
||||
setPreviewUrl(customUrl);
|
||||
} catch {
|
||||
toast.error('Please enter a valid URL');
|
||||
}
|
||||
}
|
||||
};
|
||||
const presetColors = [
|
||||
{ bg: '#6366F1', icon: '#FFFFFF', name: 'Indigo' },
|
||||
{ bg: '#10B981', icon: '#FFFFFF', name: 'Emerald' },
|
||||
{ bg: '#F59E0B', icon: '#FFFFFF', name: 'Amber' },
|
||||
{ bg: '#EF4444', icon: '#FFFFFF', name: 'Red' },
|
||||
{ bg: '#8B5CF6', icon: '#FFFFFF', name: 'Purple' },
|
||||
];
|
||||
|
||||
const ColorControls = () => (
|
||||
<div className="space-y-6">
|
||||
|
||||
const handleRemoveImage = () => {
|
||||
onImageUpdate(null);
|
||||
toast.success('Profile image removed');
|
||||
onClose();
|
||||
};
|
||||
|
||||
const handleClose = () => {
|
||||
setCustomUrl('');
|
||||
setPreviewUrl(null);
|
||||
setDragActive(false);
|
||||
onClose();
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[400px]">
|
||||
<DialogHeader className="pb-2">
|
||||
<DialogTitle className="text-center">Profile Picture</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6">
|
||||
{/* Current Image Display */}
|
||||
<div className="flex justify-center">
|
||||
<Avatar className="h-24 w-24 rounded-xl ring-2 ring-border">
|
||||
{currentImageUrl ? (
|
||||
<AvatarImage
|
||||
src={currentImageUrl}
|
||||
alt={agentName}
|
||||
className="object-cover w-full h-full"
|
||||
/>
|
||||
) : (
|
||||
<AvatarFallback className="rounded-xl bg-muted">
|
||||
<ImageIcon className="h-10 w-10 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
)}
|
||||
</Avatar>
|
||||
</div>
|
||||
|
||||
<Tabs defaultValue="upload" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-2 h-9">
|
||||
<TabsTrigger value="upload" className="text-sm">
|
||||
Upload
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="url" className="text-sm">
|
||||
URL
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="upload" className="mt-4">
|
||||
<div
|
||||
className={`border-2 border-dashed rounded-lg p-6 text-center transition-all cursor-pointer ${
|
||||
dragActive
|
||||
? 'border-primary bg-primary/5 scale-[1.02]'
|
||||
: 'border-muted-foreground/25 hover:border-muted-foreground/50 hover:bg-muted/25'
|
||||
} ${isUploading ? 'opacity-50 cursor-not-allowed' : ''}`}
|
||||
onDrop={handleDrop}
|
||||
onDragOver={handleDragOver}
|
||||
onDragLeave={handleDragLeave}
|
||||
onClick={() => !isUploading && document.getElementById('file-upload')?.click()}
|
||||
>
|
||||
<Upload className={`mx-auto h-8 w-8 mb-3 transition-colors ${
|
||||
dragActive ? 'text-primary' : 'text-muted-foreground'
|
||||
}`} />
|
||||
<p className="text-sm font-medium mb-1">
|
||||
{isUploading ? 'Uploading...' : 'Drop image here or click to select'}
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
PNG, JPG, GIF up to 5MB
|
||||
</p>
|
||||
<input
|
||||
type="file"
|
||||
accept="image/*"
|
||||
onChange={handleFileInputChange}
|
||||
className="hidden"
|
||||
id="file-upload"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="url" className="mt-4 space-y-4">
|
||||
<div className="space-y-3">
|
||||
<Input
|
||||
type="url"
|
||||
placeholder="Paste image URL here..."
|
||||
value={customUrl}
|
||||
onChange={(e) => setCustomUrl(e.target.value)}
|
||||
onBlur={handleUrlPreview}
|
||||
className="text-sm"
|
||||
/>
|
||||
|
||||
{previewUrl && (
|
||||
<div className="flex justify-center p-3 bg-muted/30 rounded-lg">
|
||||
<Avatar className="h-12 w-12 rounded-lg">
|
||||
<AvatarImage
|
||||
src={previewUrl}
|
||||
alt="Preview"
|
||||
className="object-cover w-full h-full"
|
||||
onError={() => {
|
||||
setPreviewUrl(null);
|
||||
toast.error('Unable to load image from URL');
|
||||
}}
|
||||
/>
|
||||
<AvatarFallback className="rounded-lg">
|
||||
<ImageIcon className="h-5 w-5 text-muted-foreground" />
|
||||
</AvatarFallback>
|
||||
</Avatar>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
onClick={handleUrlSubmit}
|
||||
disabled={!customUrl.trim() || isUrlSubmitting}
|
||||
className="w-full"
|
||||
size="sm"
|
||||
>
|
||||
{isUrlSubmitting ? 'Updating...' : 'Set as Profile Picture'}
|
||||
</Button>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* Action Buttons */}
|
||||
<div className="flex justify-between pt-2">
|
||||
{currentImageUrl ? (
|
||||
<Button
|
||||
variant="ghost"
|
||||
onClick={handleRemoveImage}
|
||||
className="text-destructive hover:text-destructive hover:bg-destructive/10"
|
||||
size="sm"
|
||||
>
|
||||
Remove
|
||||
</Button>
|
||||
) : (
|
||||
<div />
|
||||
)}
|
||||
<Button variant="outline" onClick={handleClose} size="sm">
|
||||
Close
|
||||
</Button>
|
||||
<div className="flex flex-col items-center space-y-3 py-4">
|
||||
<AgentIconAvatar
|
||||
iconName={selectedIcon}
|
||||
iconColor={iconColor}
|
||||
backgroundColor={backgroundColor}
|
||||
agentName={agentName}
|
||||
size={100}
|
||||
className="rounded-3xl border"
|
||||
/>
|
||||
<div className="text-center">
|
||||
<p className="font-medium">{agentName || 'Agent'}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<Label htmlFor="icon-color" className="text-sm mb-2 block">Icon Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="icon-color"
|
||||
type="color"
|
||||
value={iconColor}
|
||||
onChange={(e) => setIconColor(e.target.value)}
|
||||
className="w-16 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={iconColor}
|
||||
onChange={(e) => setIconColor(e.target.value)}
|
||||
placeholder="#000000"
|
||||
className="flex-1"
|
||||
maxLength={7}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="bg-color" className="text-sm mb-2 block">Background Color</Label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="bg-color"
|
||||
type="color"
|
||||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
className="w-16 h-10 p-1 cursor-pointer"
|
||||
/>
|
||||
<Input
|
||||
type="text"
|
||||
value={backgroundColor}
|
||||
onChange={(e) => setBackgroundColor(e.target.value)}
|
||||
placeholder="#F3F4F6"
|
||||
className="flex-1"
|
||||
maxLength={7}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<Label className="text-sm">Quick Presets</Label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{presetColors.map((preset) => (
|
||||
<button
|
||||
key={preset.name}
|
||||
onClick={() => {
|
||||
setIconColor(preset.icon);
|
||||
setBackgroundColor(preset.bg);
|
||||
}}
|
||||
className="group relative h-10 w-full rounded-lg border-2 border-border hover:border-primary transition-all hover:scale-105"
|
||||
style={{ backgroundColor: preset.bg }}
|
||||
title={preset.name}
|
||||
>
|
||||
<span className="sr-only">{preset.name}</span>
|
||||
{backgroundColor === preset.bg && iconColor === preset.icon && (
|
||||
<div className="absolute inset-0 rounded-lg ring-2 ring-primary ring-offset-2" />
|
||||
)}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh] flex flex-col p-0">
|
||||
<DialogHeader className="px-6 pt-6 pb-4 shrink-0">
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Sparkles className="h-5 w-5" />
|
||||
Customize Agent Icon
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="hidden md:flex flex-1 min-h-0 px-6">
|
||||
<div className="flex gap-6 w-full">
|
||||
<div className="flex-1 min-w-0">
|
||||
<IconPicker
|
||||
selectedIcon={selectedIcon}
|
||||
onIconSelect={setSelectedIcon}
|
||||
iconColor={iconColor}
|
||||
backgroundColor={backgroundColor}
|
||||
className="h-full"
|
||||
/>
|
||||
</div>
|
||||
<Separator orientation="vertical" className="h-full" />
|
||||
<div className="w-80 shrink-0">
|
||||
<ScrollArea className="h-[500px] pr-4">
|
||||
<ColorControls />
|
||||
</ScrollArea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="md:hidden flex-1 min-h-0 px-6">
|
||||
<Tabs defaultValue="customize" className="h-full flex flex-col">
|
||||
<TabsList className="grid w-full grid-cols-2 shrink-0">
|
||||
<TabsTrigger value="customize">Customize</TabsTrigger>
|
||||
<TabsTrigger value="icons">Icons</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="customize" className="flex-1 min-h-0 mt-4">
|
||||
<ScrollArea className="h-[400px]">
|
||||
<ColorControls />
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
<TabsContent value="icons" className="flex-1 min-h-0 mt-4">
|
||||
<IconPicker
|
||||
selectedIcon={selectedIcon}
|
||||
onIconSelect={setSelectedIcon}
|
||||
iconColor={iconColor}
|
||||
backgroundColor={backgroundColor}
|
||||
className="h-[400px]"
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</div>
|
||||
<DialogFooter className="px-6 py-4 shrink-0 border-t">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={onClose}
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleIconSave}
|
||||
>
|
||||
Save Icon
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import React from 'react';
|
||||
import { Download, CheckCircle, Loader2, Globe, GlobeLock, GitBranch, Trash2, MoreVertical, User } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import {
|
||||
|
@ -9,7 +10,6 @@ import {
|
|||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuSeparator,
|
||||
} from '@/components/ui/dropdown-menu';
|
||||
import {
|
||||
AlertDialog,
|
||||
|
@ -21,7 +21,6 @@ import {
|
|||
AlertDialogHeader,
|
||||
AlertDialogTitle,
|
||||
} from '@/components/ui/alert-dialog';
|
||||
import type { MarketplaceTemplate } from '@/components/agents/installation/types';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
|
||||
export type AgentCardMode = 'marketplace' | 'template' | 'agent';
|
||||
|
@ -338,7 +337,21 @@ const TemplateActions: React.FC<{
|
|||
</div>
|
||||
);
|
||||
|
||||
const CardAvatar: React.FC<{ isSunaAgent?: boolean; profileImageUrl?: string; agentName?: string }> = ({ isSunaAgent = false, profileImageUrl, agentName }) => {
|
||||
const CardAvatar: React.FC<{
|
||||
isSunaAgent?: boolean;
|
||||
profileImageUrl?: string;
|
||||
agentName?: string;
|
||||
iconName?: string;
|
||||
iconColor?: string;
|
||||
iconBackground?: string;
|
||||
}> = ({
|
||||
isSunaAgent = false,
|
||||
profileImageUrl,
|
||||
agentName,
|
||||
iconName,
|
||||
iconColor = '#000000',
|
||||
iconBackground = '#F3F4F6'
|
||||
}) => {
|
||||
if (isSunaAgent) {
|
||||
return (
|
||||
<div className="h-14 w-14 bg-muted border flex items-center justify-center rounded-2xl">
|
||||
|
@ -346,11 +359,28 @@ const CardAvatar: React.FC<{ isSunaAgent?: boolean; profileImageUrl?: string; ag
|
|||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
if (iconName) {
|
||||
return (
|
||||
<div
|
||||
className="h-14 w-14 flex items-center justify-center rounded-2xl"
|
||||
style={{ backgroundColor: iconBackground }}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={iconName as any}
|
||||
size={28}
|
||||
color={iconColor}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (profileImageUrl) {
|
||||
return (
|
||||
<img src={profileImageUrl} alt="Agent" className="h-14 w-14 rounded-2xl object-cover" />
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="h-14 w-14 bg-muted border flex items-center justify-center rounded-2xl">
|
||||
<span className="text-lg font-semibold">{agentName?.charAt(0).toUpperCase() || '?'}</span>
|
||||
|
@ -454,7 +484,14 @@ export const AgentCard: React.FC<AgentCardProps> = ({
|
|||
<div className="absolute inset-0 bg-gradient-to-br from-primary/5 to-transparent opacity-0 group-hover:opacity-100 transition-opacity" />
|
||||
<div className="relative p-6 flex flex-col flex-1">
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<CardAvatar isSunaAgent={isSunaAgent} profileImageUrl={(data as any)?.profile_image_url} agentName={data.name} />
|
||||
<CardAvatar
|
||||
isSunaAgent={isSunaAgent}
|
||||
profileImageUrl={(data as any)?.profile_image_url}
|
||||
agentName={data.name}
|
||||
iconName={(data as any)?.icon_name}
|
||||
iconColor={(data as any)?.icon_color}
|
||||
iconBackground={(data as any)?.icon_background}
|
||||
/>
|
||||
<div className="flex items-center gap-2">
|
||||
{renderBadge()}
|
||||
</div>
|
||||
|
|
|
@ -11,6 +11,9 @@ export interface MarketplaceTemplate {
|
|||
profile_image_url?: string;
|
||||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
icon_name?: string;
|
||||
icon_color?: string;
|
||||
icon_background?: string;
|
||||
template_id: string;
|
||||
is_kortix_team?: boolean;
|
||||
model?: string;
|
||||
|
|
|
@ -6,6 +6,7 @@ import { Badge } from '@/components/ui/badge';
|
|||
import { Button } from '@/components/ui/button';
|
||||
import { Card, CardContent } from '@/components/ui/card';
|
||||
import { Bot, Download, Wrench, Plug, Tag, User, Calendar, Loader2, Share, Cpu, Eye, Zap } from 'lucide-react';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
import { toast } from 'sonner';
|
||||
import type { MarketplaceTemplate } from '@/components/agents/installation/types';
|
||||
import { useComposioToolkitIcon } from '@/hooks/react-query/composio/use-composio';
|
||||
|
@ -150,12 +151,31 @@ export const MarketplaceAgentPreviewDialog: React.FC<MarketplaceAgentPreviewDial
|
|||
return qualifiedName.replace(/\b\w/g, l => l.toUpperCase());
|
||||
};
|
||||
|
||||
console.log('agent', agent);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={onClose}>
|
||||
<DialogContent className="max-w-2xl max-h-[90vh] p-0 overflow-hidden">
|
||||
<DialogHeader className='p-6'>
|
||||
<DialogTitle className='sr-only'>Agent Preview</DialogTitle>
|
||||
{agent.profile_image_url ? (
|
||||
{agent.icon_name ? (
|
||||
<div
|
||||
className='relative h-20 w-20 aspect-square rounded-2xl flex items-center justify-center shadow-lg'
|
||||
style={{ backgroundColor: agent.icon_background || '#e5e5e5' }}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={agent.icon_name as any}
|
||||
size={40}
|
||||
color={agent.icon_color || '#000000'}
|
||||
/>
|
||||
<div
|
||||
className="absolute inset-0 rounded-2xl pointer-events-none opacity-0 dark:opacity-100 transition-opacity"
|
||||
style={{
|
||||
boxShadow: `0 16px 48px -8px ${agent.icon_background || '#e5e5e5'}70, 0 8px 24px -4px ${agent.icon_background || '#e5e5e5'}50`
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : agent.profile_image_url ? (
|
||||
<img
|
||||
src={agent.profile_image_url}
|
||||
alt={agent.name}
|
||||
|
|
|
@ -43,7 +43,6 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
|
|||
const [installingItemId, setInstallingItemId] = React.useState<string | null>(null);
|
||||
|
||||
const handleCardClick = (template: any) => {
|
||||
// Map the template to MarketplaceTemplate format
|
||||
const marketplaceTemplate: MarketplaceTemplate = {
|
||||
id: template.template_id,
|
||||
template_id: template.template_id,
|
||||
|
@ -58,6 +57,9 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
|
|||
profile_image_url: template.profile_image_url,
|
||||
avatar: template.avatar,
|
||||
avatar_color: template.avatar_color,
|
||||
icon_name: template.icon_name,
|
||||
icon_color: template.icon_color,
|
||||
icon_background: template.icon_background,
|
||||
mcp_requirements: template.mcp_requirements || [],
|
||||
agentpress_tools: template.agentpress_tools || {},
|
||||
model: template.metadata?.model,
|
||||
|
@ -68,7 +70,6 @@ export function CustomAgentsSection({ onAgentSelect }: CustomAgentsSectionProps)
|
|||
setIsPreviewOpen(true);
|
||||
};
|
||||
|
||||
// Handle clicking Install from the preview dialog - opens the streamlined install dialog
|
||||
const handlePreviewInstall = (agent: MarketplaceTemplate) => {
|
||||
setIsPreviewOpen(false);
|
||||
setSelectedTemplate(agent);
|
||||
|
|
|
@ -4,14 +4,12 @@ import React, { useState, useMemo } from 'react';
|
|||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { CsvTable } from '@/components/ui/csv-table';
|
||||
import Papa from 'papaparse';
|
||||
import { cn } from '@/lib/utils';
|
||||
import {
|
||||
Search,
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
import {
|
||||
Search,
|
||||
FileSpreadsheet,
|
||||
ArrowUpDown,
|
||||
Filter,
|
||||
} from 'lucide-react';
|
||||
import {
|
||||
|
@ -49,8 +47,8 @@ function parseCSV(content: string) {
|
|||
headers = results.meta.fields || [];
|
||||
}
|
||||
|
||||
return {
|
||||
headers,
|
||||
return {
|
||||
headers,
|
||||
data: results.data,
|
||||
meta: results.meta
|
||||
};
|
||||
|
@ -68,7 +66,7 @@ export function CsvRenderer({
|
|||
const [sortConfig, setSortConfig] = useState<SortConfig>({ column: '', direction: null });
|
||||
const [hiddenColumns, setHiddenColumns] = useState<Set<string>>(new Set());
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const [rowsPerPage] = useState(50);
|
||||
const [rowsPerPage] = useState(50);
|
||||
|
||||
const parsedData = parseCSV(content);
|
||||
const isEmpty = parsedData.data.length === 0;
|
||||
|
@ -88,18 +86,18 @@ export function CsvRenderer({
|
|||
filtered = [...filtered].sort((a: any, b: any) => {
|
||||
const aVal = a[sortConfig.column];
|
||||
const bVal = b[sortConfig.column];
|
||||
|
||||
|
||||
if (aVal == null && bVal == null) return 0;
|
||||
if (aVal == null) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
if (bVal == null) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
|
||||
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
return sortConfig.direction === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
}
|
||||
|
||||
|
||||
const aStr = String(aVal).toLowerCase();
|
||||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
|
||||
if (aStr < bStr) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
if (aStr > bStr) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
|
@ -139,36 +137,6 @@ export function CsvRenderer({
|
|||
});
|
||||
};
|
||||
|
||||
const getSortIcon = (column: string) => {
|
||||
if (sortConfig.column !== column) {
|
||||
return <ArrowUpDown className="h-3 w-3 text-muted-foreground" />;
|
||||
}
|
||||
return sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="h-3 w-3 text-primary" /> :
|
||||
<ChevronDown className="h-3 w-3 text-primary" />;
|
||||
};
|
||||
|
||||
const formatCellValue = (value: any) => {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'number') {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const getCellClassName = (value: any) => {
|
||||
if (typeof value === 'number') {
|
||||
return 'text-right font-mono';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<div className={cn('w-full h-full flex items-center justify-center', className)}>
|
||||
|
@ -199,12 +167,12 @@ export function CsvRenderer({
|
|||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Badge variant="outline" className="text-xs">
|
||||
Page {currentPage} of {totalPages}
|
||||
</Badge>
|
||||
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="outline" size="sm">
|
||||
|
@ -245,73 +213,14 @@ export function CsvRenderer({
|
|||
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="w-full h-full overflow-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
|
||||
<table className="w-full border-collapse table-fixed" style={{ minWidth: `${visibleHeaders.length * 150}px` }}>
|
||||
<thead className="bg-muted/50 sticky top-0 z-10">
|
||||
<tr>
|
||||
{visibleHeaders.map((header, index) => (
|
||||
<th
|
||||
key={header}
|
||||
className="px-4 py-3 text-left font-medium border-b border-border bg-muted/50 backdrop-blur-sm"
|
||||
style={{ width: '150px', minWidth: '150px' }}
|
||||
>
|
||||
<button
|
||||
onClick={() => handleSort(header)}
|
||||
className="flex items-center gap-2 hover:text-primary transition-colors group w-full text-left"
|
||||
>
|
||||
<span className="truncate">{header}</span>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
||||
{getSortIcon(header)}
|
||||
</div>
|
||||
</button>
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedData.map((row: any, rowIndex) => (
|
||||
<tr
|
||||
key={startIndex + rowIndex}
|
||||
className="border-b border-border hover:bg-muted/30 transition-colors"
|
||||
>
|
||||
{visibleHeaders.map((header, cellIndex) => {
|
||||
const value = row[header];
|
||||
return (
|
||||
<td
|
||||
key={`${startIndex + rowIndex}-${cellIndex}`}
|
||||
className={cn(
|
||||
"px-4 py-3 text-sm border-r border-border last:border-r-0",
|
||||
getCellClassName(value)
|
||||
)}
|
||||
style={{ width: '150px', minWidth: '150px' }}
|
||||
>
|
||||
<div className="truncate" title={String(value || '')}>
|
||||
{formatCellValue(value)}
|
||||
</div>
|
||||
</td>
|
||||
);
|
||||
})}
|
||||
</tr>
|
||||
))}
|
||||
|
||||
{/* Empty state for current page */}
|
||||
{paginatedData.length === 0 && searchTerm && (
|
||||
<tr>
|
||||
<td colSpan={visibleHeaders.length} className="py-8 text-center text-muted-foreground">
|
||||
<div className="space-y-2">
|
||||
<p>No results found for "{searchTerm}"</p>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setSearchTerm('')}
|
||||
>
|
||||
Clear search
|
||||
</Button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
<CsvTable
|
||||
headers={visibleHeaders}
|
||||
data={paginatedData}
|
||||
sortConfig={sortConfig}
|
||||
onSort={handleSort}
|
||||
searchTerm={searchTerm}
|
||||
onClearSearch={() => setSearchTerm('')}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
@ -322,7 +231,7 @@ export function CsvRenderer({
|
|||
<div className="text-sm text-muted-foreground">
|
||||
Showing {startIndex + 1} to {Math.min(startIndex + rowsPerPage, processedData.length)} of {processedData.length.toLocaleString()} rows
|
||||
</div>
|
||||
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
|
@ -332,7 +241,7 @@ export function CsvRenderer({
|
|||
>
|
||||
Previous
|
||||
</Button>
|
||||
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
{Array.from({ length: Math.min(5, totalPages) }, (_, i) => {
|
||||
let pageNum;
|
||||
|
@ -345,7 +254,7 @@ export function CsvRenderer({
|
|||
} else {
|
||||
pageNum = currentPage - 2 + i;
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<Button
|
||||
key={pageNum}
|
||||
|
@ -359,7 +268,7 @@ export function CsvRenderer({
|
|||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
import React from 'react';
|
||||
import { useAgent } from '@/hooks/react-query/agents/use-agents';
|
||||
import { KortixLogo } from '@/components/sidebar/kortix-logo';
|
||||
import { Skeleton } from '@/components/ui/skeleton';
|
||||
import { DynamicIcon } from 'lucide-react/dynamic';
|
||||
|
||||
interface AgentAvatarProps {
|
||||
agentId?: string;
|
||||
|
@ -38,6 +38,25 @@ export const AgentAvatar: React.FC<AgentAvatarProps> = ({
|
|||
return <KortixLogo size={size} />;
|
||||
}
|
||||
|
||||
if (agent?.icon_name) {
|
||||
return (
|
||||
<div
|
||||
className={`flex items-center justify-center rounded ${className}`}
|
||||
style={{
|
||||
width: size,
|
||||
height: size,
|
||||
backgroundColor: agent.icon_background || '#F3F4F6'
|
||||
}}
|
||||
>
|
||||
<DynamicIcon
|
||||
name={agent.icon_name as any}
|
||||
size={size * 0.6}
|
||||
color={agent.icon_color || '#000000'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (agent?.profile_image_url) {
|
||||
return (
|
||||
<img
|
||||
|
@ -49,7 +68,6 @@ export const AgentAvatar: React.FC<AgentAvatarProps> = ({
|
|||
);
|
||||
}
|
||||
|
||||
|
||||
return <KortixLogo size={size} />;
|
||||
};
|
||||
|
||||
|
|
|
@ -453,7 +453,8 @@ export function FileAttachment({
|
|||
"rounded-xl border bg-card overflow-hidden pt-10", // Consistent card styling with header space
|
||||
isPdf ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[500px] sm:!min-w-[300px]" :
|
||||
isHtmlOrMd ? "!min-h-[200px] sm:min-h-0 sm:h-[400px] max-h-[600px] sm:!min-w-[300px]" :
|
||||
standalone ? "min-h-[300px] h-auto" : "h-[300px]", // Better height handling for standalone
|
||||
isCsv ? "min-h-[300px] h-full" : // Let CSV take full height
|
||||
standalone ? "min-h-[300px] h-auto" : "h-[300px]", // Better height handling for standalone
|
||||
className
|
||||
)}
|
||||
style={{
|
||||
|
@ -553,16 +554,16 @@ export function FileAttachment({
|
|||
</div>
|
||||
|
||||
{/* Header with filename */}
|
||||
<div className="absolute top-0 left-0 right-0 bg-accent p-2 z-10 flex items-center justify-between">
|
||||
<div className="absolute top-0 left-0 right-0 bg-accent p-2 h-[40px] z-10 flex items-center justify-between">
|
||||
<div className="text-sm font-medium truncate">{filename}</div>
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
{/* <button
|
||||
onClick={handleDownload}
|
||||
className="cursor-pointer p-1 rounded-full hover:bg-black/10 dark:hover:bg-white/10"
|
||||
title="Download file"
|
||||
>
|
||||
<Download size={14} />
|
||||
</button>
|
||||
</button> */}
|
||||
{onClick && (
|
||||
<button
|
||||
onClick={handleClick}
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import React, { useState } from 'react';
|
||||
import { CsvTable } from '@/components/ui/csv-table';
|
||||
import Papa from 'papaparse';
|
||||
import { cn } from '@/lib/utils';
|
||||
|
||||
|
@ -35,49 +35,75 @@ function parseCSV(content: string) {
|
|||
}
|
||||
|
||||
/**
|
||||
* CSV/TSV renderer that presents data in a table format
|
||||
* Minimal CSV renderer for thread previews using the CsvTable component
|
||||
*/
|
||||
export function CsvRenderer({
|
||||
content,
|
||||
className
|
||||
}: CsvRendererProps) {
|
||||
const [sortConfig, setSortConfig] = useState<{ column: string; direction: 'asc' | 'desc' | null }>({
|
||||
column: '',
|
||||
direction: null
|
||||
});
|
||||
|
||||
const parsedData = parseCSV(content);
|
||||
const isEmpty = parsedData.data.length === 0;
|
||||
|
||||
const handleSort = (column: string) => {
|
||||
setSortConfig(prev => {
|
||||
if (prev.column === column) {
|
||||
const newDirection = prev.direction === 'asc' ? 'desc' : prev.direction === 'desc' ? null : 'asc';
|
||||
return { column: newDirection ? column : '', direction: newDirection };
|
||||
} else {
|
||||
return { column, direction: 'asc' };
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Sort the data based on sortConfig
|
||||
const sortedData = React.useMemo(() => {
|
||||
if (!sortConfig.column || !sortConfig.direction) {
|
||||
return parsedData.data;
|
||||
}
|
||||
|
||||
return [...parsedData.data].sort((a: any, b: any) => {
|
||||
const aVal = a[sortConfig.column];
|
||||
const bVal = b[sortConfig.column];
|
||||
|
||||
if (aVal == null && bVal == null) return 0;
|
||||
if (aVal == null) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
if (bVal == null) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
|
||||
if (typeof aVal === 'number' && typeof bVal === 'number') {
|
||||
return sortConfig.direction === 'asc' ? aVal - bVal : bVal - aVal;
|
||||
}
|
||||
|
||||
const aStr = String(aVal).toLowerCase();
|
||||
const bStr = String(bVal).toLowerCase();
|
||||
|
||||
if (aStr < bStr) return sortConfig.direction === 'asc' ? -1 : 1;
|
||||
if (aStr > bStr) return sortConfig.direction === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
}, [parsedData.data, sortConfig]);
|
||||
|
||||
if (isEmpty) {
|
||||
return (
|
||||
<div className={cn('w-full h-full flex items-center justify-center', className)}>
|
||||
<div className="text-muted-foreground text-sm">No data</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn('w-full h-full overflow-hidden', className)}>
|
||||
<ScrollArea className="w-full h-full">
|
||||
<div className="p-0">
|
||||
<table className="w-full border-collapse text-sm">
|
||||
<thead className="bg-muted sticky top-0">
|
||||
<tr>
|
||||
{parsedData.headers.map((header, index) => (
|
||||
<th key={index} className="px-3 py-2 text-left font-medium border border-border">
|
||||
{header}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{!isEmpty ? parsedData.data.map((row: any, rowIndex) => (
|
||||
<tr key={rowIndex} className="border-b border-border hover:bg-muted/50">
|
||||
{parsedData.headers.map((header, cellIndex) => (
|
||||
<td key={cellIndex} className="px-3 py-2 border border-border">
|
||||
{row[header] || ''}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
)) : (
|
||||
<tr>
|
||||
<td colSpan={parsedData.headers.length || 1} className="py-4 text-center text-muted-foreground">
|
||||
No data available
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
<div className={cn('w-full h-full', className)}>
|
||||
<CsvTable
|
||||
headers={parsedData.headers}
|
||||
data={sortedData}
|
||||
sortConfig={sortConfig}
|
||||
onSort={handleSort}
|
||||
containerHeight={300} // Fixed height for thread preview
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,208 @@
|
|||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { useTheme } from 'next-themes';
|
||||
import {
|
||||
ChevronUp,
|
||||
ChevronDown,
|
||||
ArrowUpDown,
|
||||
} from 'lucide-react';
|
||||
|
||||
interface CsvTableProps {
|
||||
headers: string[];
|
||||
data: any[];
|
||||
sortConfig: { column: string; direction: 'asc' | 'desc' | null };
|
||||
onSort: (column: string) => void;
|
||||
searchTerm?: string;
|
||||
onClearSearch?: () => void;
|
||||
className?: string;
|
||||
containerHeight?: number; // Add container height prop
|
||||
}
|
||||
|
||||
export function CsvTable({
|
||||
headers,
|
||||
data,
|
||||
sortConfig,
|
||||
onSort,
|
||||
searchTerm,
|
||||
onClearSearch,
|
||||
className,
|
||||
containerHeight = 500
|
||||
}: CsvTableProps) {
|
||||
const { resolvedTheme } = useTheme();
|
||||
const ROW_HEIGHT = 48; // Height of each row in pixels
|
||||
const HEADER_HEIGHT = 48; // Height of header row
|
||||
const COL_WIDTH = 150; // Fixed column width in pixels
|
||||
|
||||
// Calculate offset to handle parent containers with fractional heights like calc(100vh-17rem)
|
||||
// 17rem = 272px (16px base * 17), so we need to offset the grid to align properly
|
||||
const parentOffset = 272; // 17rem in pixels
|
||||
const gridOffsetY = parentOffset % ROW_HEIGHT; // Get remainder to align to 48px grid
|
||||
|
||||
// Theme-aware grid colors with subtle monochrome grays
|
||||
const getGridColors = () => {
|
||||
const isDark = resolvedTheme === 'dark';
|
||||
return {
|
||||
vertical: isDark ? 'rgb(64 64 64)' : 'rgb(229 229 229)', // Subtle gray for each theme
|
||||
horizontal: isDark ? 'rgb(64 64 64)' : 'rgb(229 229 229)', // Same for consistency
|
||||
background: 'hsl(var(--muted))'
|
||||
};
|
||||
};
|
||||
|
||||
const gridColors = getGridColors();
|
||||
const getSortIcon = (column: string) => {
|
||||
if (sortConfig.column !== column) {
|
||||
return <ArrowUpDown className="h-3 w-3 text-muted-foreground" />;
|
||||
}
|
||||
return sortConfig.direction === 'asc' ?
|
||||
<ChevronUp className="h-3 w-3 text-primary" /> :
|
||||
<ChevronDown className="h-3 w-3 text-primary" />;
|
||||
};
|
||||
|
||||
const formatCellValue = (value: any) => {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'number') {
|
||||
return value.toLocaleString();
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'Yes' : 'No';
|
||||
}
|
||||
return String(value);
|
||||
};
|
||||
|
||||
const getCellClassName = (value: any) => {
|
||||
if (typeof value === 'number') {
|
||||
return 'text-right font-mono';
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return value ? 'text-green-600 dark:text-green-400' : 'text-red-600 dark:text-red-400';
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn("w-full relative h-full !bg-card", className)}>
|
||||
{/* Use CSS Grid for perfect alignment */}
|
||||
<div
|
||||
className="w-full h-full overflow-auto relative min-h-full"
|
||||
style={{
|
||||
display: 'grid',
|
||||
gridTemplateColumns: `repeat(${headers.length}, 150px)`,
|
||||
gridAutoRows: '48px',
|
||||
minHeight: '100%',
|
||||
backgroundColor: gridColors.background,
|
||||
backgroundImage: `
|
||||
repeating-linear-gradient(
|
||||
to right,
|
||||
transparent 0px,
|
||||
transparent 149px,
|
||||
${gridColors.vertical} 149px,
|
||||
${gridColors.vertical} 150px
|
||||
),
|
||||
repeating-linear-gradient(
|
||||
to bottom,
|
||||
transparent 0px,
|
||||
transparent 47px,
|
||||
${gridColors.horizontal} 47px,
|
||||
${gridColors.horizontal} 48px
|
||||
)
|
||||
`,
|
||||
backgroundSize: `${(headers.length - 1) * 150 + 149}px 48px, 150px 48px`,
|
||||
backgroundPosition: '0 0',
|
||||
backgroundAttachment: 'local'
|
||||
} as React.CSSProperties}
|
||||
>
|
||||
{/* Header background extension for infinite grid */}
|
||||
<div
|
||||
className={cn(
|
||||
"sticky top-0 z-10 pointer-events-none",
|
||||
resolvedTheme === 'dark' ? 'bg-muted' : 'bg-background'
|
||||
)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
left: `${headers.length * 150}px`,
|
||||
right: 0,
|
||||
height: '48px',
|
||||
top: 0
|
||||
}}
|
||||
/>
|
||||
{/* Header row */}
|
||||
{headers.map((header, index) => (
|
||||
<div
|
||||
key={`header-${index}`}
|
||||
className={cn(
|
||||
"sticky top-0 z-20 flex items-center px-4 font-medium",
|
||||
resolvedTheme === 'dark' ? 'bg-muted' : 'bg-background'
|
||||
)}
|
||||
style={{
|
||||
gridColumn: index + 1,
|
||||
gridRow: 1,
|
||||
height: '48px'
|
||||
}}
|
||||
>
|
||||
<button
|
||||
onClick={() => onSort(header)}
|
||||
className="flex items-center gap-2 hover:text-primary transition-colors group w-full text-left"
|
||||
>
|
||||
<span className="truncate">{header}</span>
|
||||
<div className="opacity-0 group-hover:opacity-100 transition-opacity flex-shrink-0">
|
||||
{getSortIcon(header)}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{/* Data rows */}
|
||||
{data.map((row: any, rowIndex) => (
|
||||
headers.map((header, cellIndex) => {
|
||||
const value = row[header];
|
||||
return (
|
||||
<div
|
||||
key={`${rowIndex}-${cellIndex}`}
|
||||
className={cn(
|
||||
"flex items-center px-4 text-sm hover:bg-muted/30 transition-colors",
|
||||
getCellClassName(value)
|
||||
)}
|
||||
style={{
|
||||
gridColumn: cellIndex + 1,
|
||||
gridRow: rowIndex + 2,
|
||||
height: '48px'
|
||||
}}
|
||||
>
|
||||
<div className="truncate w-full" title={String(value || '')}>
|
||||
{formatCellValue(value)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})
|
||||
))}
|
||||
|
||||
{/* Empty state for search */}
|
||||
{data.length === 0 && searchTerm && (
|
||||
<div
|
||||
className="col-span-full py-8 text-center text-muted-foreground"
|
||||
style={{
|
||||
gridColumn: `1 / ${headers.length + 1}`,
|
||||
gridRow: 2
|
||||
}}
|
||||
>
|
||||
<div className="space-y-2">
|
||||
<p>No results found for "{searchTerm}"</p>
|
||||
{onClearSearch && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={onClearSearch}
|
||||
>
|
||||
Clear search
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -71,6 +71,9 @@ export const useCreateNewAgent = () => {
|
|||
configured_mcps: [],
|
||||
agentpress_tools: DEFAULT_AGENTPRESS_TOOLS,
|
||||
is_default: false,
|
||||
icon_name: 'brain',
|
||||
icon_color: '#000000',
|
||||
icon_background: '#F3F4F6',
|
||||
};
|
||||
|
||||
const newAgent = await createAgentMutation.mutateAsync(defaultAgentData);
|
||||
|
|
|
@ -26,8 +26,10 @@ export type Agent = {
|
|||
tags?: string[];
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
// New
|
||||
profile_image_url?: string;
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
current_version_id?: string | null;
|
||||
version_count?: number;
|
||||
current_version?: AgentVersion | null;
|
||||
|
@ -95,6 +97,10 @@ export type AgentCreateRequest = {
|
|||
is_default?: boolean;
|
||||
// New
|
||||
profile_image_url?: string;
|
||||
// Icon system fields
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
};
|
||||
|
||||
export type AgentVersionCreateRequest = {
|
||||
|
@ -150,6 +156,10 @@ export type AgentUpdateRequest = {
|
|||
is_default?: boolean;
|
||||
// New
|
||||
profile_image_url?: string;
|
||||
// Icon system fields
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
};
|
||||
|
||||
export const getAgents = async (params: AgentsParams = {}): Promise<AgentsResponse> => {
|
||||
|
|
|
@ -46,11 +46,15 @@ export interface AgentTemplate {
|
|||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
profile_image_url?: string;
|
||||
icon_name?: string;
|
||||
icon_color?: string;
|
||||
icon_background?: string;
|
||||
is_kortix_team?: boolean;
|
||||
metadata?: {
|
||||
source_agent_id?: string;
|
||||
source_version_id?: string;
|
||||
source_version_name?: string;
|
||||
model?: string;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -26,6 +26,9 @@ interface NormalizedVersionData {
|
|||
updated_at: string;
|
||||
created_by?: string;
|
||||
change_description?: string;
|
||||
icon_name?: string | null;
|
||||
icon_color?: string | null;
|
||||
icon_background?: string | null;
|
||||
}
|
||||
|
||||
interface UseAgentVersionDataProps {
|
||||
|
|
|
@ -35,6 +35,10 @@ class AgentCreateRequest:
|
|||
is_default: bool = False
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
@ -47,6 +51,10 @@ class AgentUpdateRequest:
|
|||
is_default: Optional[bool] = None
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
profile_image_url: Optional[str] = None
|
||||
icon_name: Optional[str] = None
|
||||
icon_color: Optional[str] = None
|
||||
icon_background: Optional[str] = None
|
||||
|
||||
|
||||
@dataclass
|
||||
|
|
Loading…
Reference in New Issue