mirror of https://github.com/kortix-ai/suna.git
chore(dev): add styling to custom agents
This commit is contained in:
parent
8132fc59dd
commit
a95ae458ef
|
@ -51,6 +51,8 @@ class AgentCreateRequest(BaseModel):
|
|||
configured_mcps: Optional[List[Dict[str, Any]]] = []
|
||||
agentpress_tools: Optional[Dict[str, Any]] = {}
|
||||
is_default: Optional[bool] = False
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
|
||||
class AgentUpdateRequest(BaseModel):
|
||||
name: Optional[str] = None
|
||||
|
@ -59,6 +61,8 @@ class AgentUpdateRequest(BaseModel):
|
|||
configured_mcps: Optional[List[Dict[str, Any]]] = None
|
||||
agentpress_tools: Optional[Dict[str, Any]] = None
|
||||
is_default: Optional[bool] = None
|
||||
avatar: Optional[str] = None
|
||||
avatar_color: Optional[str] = None
|
||||
|
||||
class AgentResponse(BaseModel):
|
||||
agent_id: str
|
||||
|
@ -69,6 +73,8 @@ class AgentResponse(BaseModel):
|
|||
configured_mcps: List[Dict[str, Any]]
|
||||
agentpress_tools: Dict[str, Any]
|
||||
is_default: bool
|
||||
avatar: Optional[str]
|
||||
avatar_color: Optional[str]
|
||||
created_at: str
|
||||
updated_at: str
|
||||
|
||||
|
@ -528,6 +534,8 @@ async def get_thread_agent(thread_id: str, user_id: str = Depends(get_current_us
|
|||
configured_mcps=agent_data.get('configured_mcps', []),
|
||||
agentpress_tools=agent_data.get('agentpress_tools', {}),
|
||||
is_default=agent_data.get('is_default', False),
|
||||
avatar=agent_data.get('avatar'),
|
||||
avatar_color=agent_data.get('avatar_color'),
|
||||
created_at=agent_data['created_at'],
|
||||
updated_at=agent_data['updated_at']
|
||||
),
|
||||
|
@ -1007,6 +1015,8 @@ async def get_agents(user_id: str = Depends(get_current_user_id_from_jwt)):
|
|||
configured_mcps=agent.get('configured_mcps', []),
|
||||
agentpress_tools=agent.get('agentpress_tools', {}),
|
||||
is_default=agent.get('is_default', False),
|
||||
avatar=agent.get('avatar'),
|
||||
avatar_color=agent.get('avatar_color'),
|
||||
created_at=agent['created_at'],
|
||||
updated_at=agent['updated_at']
|
||||
))
|
||||
|
@ -1041,6 +1051,8 @@ async def get_agent(agent_id: str, user_id: str = Depends(get_current_user_id_fr
|
|||
configured_mcps=agent_data.get('configured_mcps', []),
|
||||
agentpress_tools=agent_data.get('agentpress_tools', {}),
|
||||
is_default=agent_data.get('is_default', False),
|
||||
avatar=agent_data.get('avatar'),
|
||||
avatar_color=agent_data.get('avatar_color'),
|
||||
created_at=agent_data['created_at'],
|
||||
updated_at=agent_data['updated_at']
|
||||
)
|
||||
|
@ -1078,7 +1090,9 @@ async def create_agent(
|
|||
"system_prompt": agent_data.system_prompt,
|
||||
"configured_mcps": agent_data.configured_mcps or [],
|
||||
"agentpress_tools": agent_data.agentpress_tools or {},
|
||||
"is_default": agent_data.is_default or False
|
||||
"is_default": agent_data.is_default or False,
|
||||
"avatar": agent_data.avatar,
|
||||
"avatar_color": agent_data.avatar_color
|
||||
}
|
||||
|
||||
new_agent = await client.table('agents').insert(insert_data).execute()
|
||||
|
@ -1098,6 +1112,8 @@ async def create_agent(
|
|||
configured_mcps=agent.get('configured_mcps', []),
|
||||
agentpress_tools=agent.get('agentpress_tools', {}),
|
||||
is_default=agent.get('is_default', False),
|
||||
avatar=agent.get('avatar'),
|
||||
avatar_color=agent.get('avatar_color'),
|
||||
created_at=agent['created_at'],
|
||||
updated_at=agent['updated_at']
|
||||
)
|
||||
|
@ -1150,6 +1166,10 @@ async def update_agent(
|
|||
# If setting as default, unset other defaults first
|
||||
if agent_data.is_default:
|
||||
await client.table('agents').update({"is_default": False}).eq("account_id", user_id).eq("is_default", True).neq("agent_id", agent_id).execute()
|
||||
if agent_data.avatar is not None:
|
||||
update_data["avatar"] = agent_data.avatar
|
||||
if agent_data.avatar_color is not None:
|
||||
update_data["avatar_color"] = agent_data.avatar_color
|
||||
|
||||
if not update_data:
|
||||
# No fields to update, return existing agent
|
||||
|
@ -1180,6 +1200,8 @@ async def update_agent(
|
|||
configured_mcps=agent.get('configured_mcps', []),
|
||||
agentpress_tools=agent.get('agentpress_tools', {}),
|
||||
is_default=agent.get('is_default', False),
|
||||
avatar=agent.get('avatar'),
|
||||
avatar_color=agent.get('avatar_color'),
|
||||
created_at=agent['created_at'],
|
||||
updated_at=agent['updated_at']
|
||||
)
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
BEGIN;
|
||||
|
||||
ALTER TABLE agents
|
||||
ADD COLUMN IF NOT EXISTS avatar VARCHAR(10),
|
||||
ADD COLUMN IF NOT EXISTS avatar_color VARCHAR(7);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_agents_avatar ON agents(avatar);
|
||||
CREATE INDEX IF NOT EXISTS idx_agents_avatar_color ON agents(avatar_color);
|
||||
|
||||
COMMENT ON COLUMN agents.avatar IS 'Emoji character used as agent avatar (e.g., 🤖)';
|
||||
COMMENT ON COLUMN agents.avatar_color IS 'Hex color code for agent avatar background (e.g., #3b82f6)';
|
||||
|
||||
COMMIT;
|
|
@ -1,8 +1,19 @@
|
|||
import React from 'react';
|
||||
import { Bot, Calendar, Settings, Sparkles, User } from 'lucide-react';
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { DEFAULT_AGENTPRESS_TOOLS, getToolDisplayName } from '../_data/tools';
|
||||
import { cn } from '@/lib/utils';
|
||||
import { toast } from 'sonner';
|
||||
import { getAgentAvatar } from '../_utils/get-agent-style';
|
||||
import {
|
||||
ChatInput,
|
||||
ChatInputHandles
|
||||
} from '@/components/thread/chat-input/chat-input';
|
||||
import { ThreadContent } from '@/components/thread/content/ThreadContent';
|
||||
import { UnifiedMessage } from '@/components/thread/types';
|
||||
import { useInitiateAgentWithInvalidation } from '@/hooks/react-query/dashboard/use-initiate-agent';
|
||||
import { useAgentStream } from '@/hooks/useAgentStream';
|
||||
import { useAddUserMessageMutation } from '@/hooks/react-query/threads/use-messages';
|
||||
import { useStartAgentMutation, useStopAgentMutation } from '@/hooks/react-query/threads/use-agent-run';
|
||||
import { BillingError } from '@/lib/api';
|
||||
|
||||
interface Agent {
|
||||
agent_id: string;
|
||||
|
@ -21,15 +32,349 @@ interface AgentPreviewProps {
|
|||
}
|
||||
|
||||
export const AgentPreview = ({ agent }: AgentPreviewProps) => {
|
||||
const enabledTools = Object.entries(agent.agentpress_tools || {})
|
||||
.filter(([_, tool]) => tool.enabled)
|
||||
.map(([toolName]) => toolName);
|
||||
const [messages, setMessages] = useState<UnifiedMessage[]>([]);
|
||||
const [inputValue, setInputValue] = useState('');
|
||||
const [threadId, setThreadId] = useState<string | null>(null);
|
||||
const [agentRunId, setAgentRunId] = useState<string | null>(null);
|
||||
const [agentStatus, setAgentStatus] = useState<'idle' | 'running' | 'connecting' | 'error'>('idle');
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
const [hasStartedConversation, setHasStartedConversation] = useState(false);
|
||||
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const chatInputRef = useRef<ChatInputHandles>(null);
|
||||
|
||||
const getAgentStyling = () => {
|
||||
const agentData = agent as any;
|
||||
if (agentData.avatar && agentData.avatar_color) {
|
||||
return {
|
||||
avatar: agentData.avatar,
|
||||
color: agentData.avatar_color,
|
||||
};
|
||||
}
|
||||
return getAgentAvatar(agent.agent_id);
|
||||
};
|
||||
|
||||
const { avatar, color } = getAgentStyling();
|
||||
|
||||
const initiateAgentMutation = useInitiateAgentWithInvalidation();
|
||||
const addUserMessageMutation = useAddUserMessageMutation();
|
||||
const startAgentMutation = useStartAgentMutation();
|
||||
const stopAgentMutation = useStopAgentMutation();
|
||||
|
||||
const scrollToBottom = () => {
|
||||
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
scrollToBottom();
|
||||
}, [messages]);
|
||||
|
||||
const handleNewMessageFromStream = useCallback((message: UnifiedMessage) => {
|
||||
console.log(`[PREVIEW STREAM] Received message: ID=${message.message_id}, Type=${message.type}`);
|
||||
|
||||
setMessages((prev) => {
|
||||
const messageExists = prev.some((m) => m.message_id === message.message_id);
|
||||
if (messageExists) {
|
||||
return prev.map((m) => m.message_id === message.message_id ? message : m);
|
||||
} else {
|
||||
return [...prev, message];
|
||||
}
|
||||
});
|
||||
}, []);
|
||||
|
||||
const handleStreamStatusChange = useCallback((hookStatus: string) => {
|
||||
console.log(`[PREVIEW] Stream status changed: ${hookStatus}`);
|
||||
switch (hookStatus) {
|
||||
case 'idle':
|
||||
case 'completed':
|
||||
case 'stopped':
|
||||
case 'agent_not_running':
|
||||
case 'error':
|
||||
case 'failed':
|
||||
setAgentStatus('idle');
|
||||
setAgentRunId(null);
|
||||
break;
|
||||
case 'connecting':
|
||||
setAgentStatus('connecting');
|
||||
break;
|
||||
case 'streaming':
|
||||
setAgentStatus('running');
|
||||
break;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStreamError = useCallback((errorMessage: string) => {
|
||||
console.error(`[PREVIEW] Stream error: ${errorMessage}`);
|
||||
if (!errorMessage.toLowerCase().includes('not found') &&
|
||||
!errorMessage.toLowerCase().includes('agent run is not running')) {
|
||||
toast.error(`Stream Error: ${errorMessage}`);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleStreamClose = useCallback(() => {
|
||||
console.log(`[PREVIEW] Stream closed`);
|
||||
}, []);
|
||||
|
||||
const {
|
||||
status: streamHookStatus,
|
||||
textContent: streamingTextContent,
|
||||
toolCall: streamingToolCall,
|
||||
error: streamError,
|
||||
agentRunId: currentHookRunId,
|
||||
startStreaming,
|
||||
stopStreaming,
|
||||
} = useAgentStream(
|
||||
{
|
||||
onMessage: handleNewMessageFromStream,
|
||||
onStatusChange: handleStreamStatusChange,
|
||||
onError: handleStreamError,
|
||||
onClose: handleStreamClose,
|
||||
},
|
||||
threadId,
|
||||
setMessages,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (agentRunId && agentRunId !== currentHookRunId && threadId) {
|
||||
console.log(`[PREVIEW] Starting stream for agentRunId: ${agentRunId}, threadId: ${threadId}`);
|
||||
startStreaming(agentRunId);
|
||||
}
|
||||
}, [agentRunId, startStreaming, currentHookRunId, threadId]);
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[PREVIEW] State update:', {
|
||||
threadId,
|
||||
agentRunId,
|
||||
currentHookRunId,
|
||||
agentStatus,
|
||||
streamHookStatus,
|
||||
messagesCount: messages.length,
|
||||
hasStartedConversation
|
||||
});
|
||||
}, [threadId, agentRunId, currentHookRunId, agentStatus, streamHookStatus, messages.length, hasStartedConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
if (streamingTextContent) {
|
||||
scrollToBottom();
|
||||
}
|
||||
}, [streamingTextContent]);
|
||||
|
||||
const handleSubmitFirstMessage = async (
|
||||
message: string,
|
||||
options?: {
|
||||
model_name?: string;
|
||||
enable_thinking?: boolean;
|
||||
reasoning_effort?: string;
|
||||
stream?: boolean;
|
||||
enable_context_manager?: boolean;
|
||||
},
|
||||
) => {
|
||||
if (!message.trim() && !chatInputRef.current?.getPendingFiles().length) return;
|
||||
|
||||
setIsSubmitting(true);
|
||||
setHasStartedConversation(true);
|
||||
|
||||
try {
|
||||
const files = chatInputRef.current?.getPendingFiles() || [];
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('prompt', message);
|
||||
formData.append('agent_id', agent.agent_id);
|
||||
|
||||
files.forEach((file, index) => {
|
||||
formData.append('files', file, file.name);
|
||||
});
|
||||
|
||||
if (options?.model_name) formData.append('model_name', options.model_name);
|
||||
formData.append('enable_thinking', String(options?.enable_thinking ?? false));
|
||||
formData.append('reasoning_effort', options?.reasoning_effort ?? 'low');
|
||||
formData.append('stream', String(options?.stream ?? true));
|
||||
formData.append('enable_context_manager', String(options?.enable_context_manager ?? false));
|
||||
|
||||
console.log('[PREVIEW] Initiating agent...');
|
||||
const result = await initiateAgentMutation.mutateAsync(formData);
|
||||
console.log('[PREVIEW] Agent initiated:', result);
|
||||
|
||||
if (result.thread_id) {
|
||||
setThreadId(result.thread_id);
|
||||
if (result.agent_run_id) {
|
||||
console.log('[PREVIEW] Setting agent run ID:', result.agent_run_id);
|
||||
setAgentRunId(result.agent_run_id);
|
||||
} else {
|
||||
console.log('[PREVIEW] No agent_run_id in result, starting agent manually...');
|
||||
try {
|
||||
const agentResult = await startAgentMutation.mutateAsync({
|
||||
threadId: result.thread_id,
|
||||
options
|
||||
});
|
||||
console.log('[PREVIEW] Agent started manually:', agentResult);
|
||||
setAgentRunId(agentResult.agent_run_id);
|
||||
} catch (startError) {
|
||||
console.error('[PREVIEW] Error starting agent manually:', startError);
|
||||
toast.error('Failed to start agent');
|
||||
}
|
||||
}
|
||||
const userMessage: UnifiedMessage = {
|
||||
message_id: `user-${Date.now()}`,
|
||||
thread_id: result.thread_id,
|
||||
type: 'user',
|
||||
is_llm_message: false,
|
||||
content: message,
|
||||
metadata: '{}',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
setMessages([userMessage]);
|
||||
}
|
||||
|
||||
chatInputRef.current?.clearPendingFiles();
|
||||
setInputValue('');
|
||||
} catch (error: any) {
|
||||
console.error('[PREVIEW] Error during initiation:', error);
|
||||
if (error instanceof BillingError) {
|
||||
toast.error('Billing limit reached. Please upgrade your plan.');
|
||||
} else {
|
||||
toast.error('Failed to start conversation');
|
||||
}
|
||||
setHasStartedConversation(false);
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSubmitMessage = useCallback(
|
||||
async (
|
||||
message: string,
|
||||
options?: { model_name?: string; enable_thinking?: boolean },
|
||||
) => {
|
||||
if (!message.trim() || !threadId) return;
|
||||
setIsSubmitting(true);
|
||||
|
||||
const optimisticUserMessage: UnifiedMessage = {
|
||||
message_id: `temp-${Date.now()}`,
|
||||
thread_id: threadId,
|
||||
type: 'user',
|
||||
is_llm_message: false,
|
||||
content: message,
|
||||
metadata: '{}',
|
||||
created_at: new Date().toISOString(),
|
||||
updated_at: new Date().toISOString(),
|
||||
};
|
||||
|
||||
setMessages((prev) => [...prev, optimisticUserMessage]);
|
||||
setInputValue('');
|
||||
|
||||
try {
|
||||
const messagePromise = addUserMessageMutation.mutateAsync({
|
||||
threadId,
|
||||
message
|
||||
});
|
||||
|
||||
const agentPromise = startAgentMutation.mutateAsync({
|
||||
threadId,
|
||||
options
|
||||
});
|
||||
|
||||
const results = await Promise.allSettled([messagePromise, agentPromise]);
|
||||
|
||||
if (results[0].status === 'rejected') {
|
||||
throw new Error(`Failed to send message: ${results[0].reason?.message || results[0].reason}`);
|
||||
}
|
||||
|
||||
if (results[1].status === 'rejected') {
|
||||
const error = results[1].reason;
|
||||
if (error instanceof BillingError) {
|
||||
toast.error('Billing limit reached. Please upgrade your plan.');
|
||||
setMessages(prev => prev.filter(m => m.message_id !== optimisticUserMessage.message_id));
|
||||
return;
|
||||
}
|
||||
throw new Error(`Failed to start agent: ${error?.message || error}`);
|
||||
}
|
||||
|
||||
const agentResult = results[1].value;
|
||||
setAgentRunId(agentResult.agent_run_id);
|
||||
|
||||
} catch (err) {
|
||||
console.error('[PREVIEW] Error sending message:', err);
|
||||
toast.error(err instanceof Error ? err.message : 'Operation failed');
|
||||
setMessages((prev) => prev.filter((m) => m.message_id !== optimisticUserMessage.message_id));
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
},
|
||||
[threadId, addUserMessageMutation, startAgentMutation],
|
||||
);
|
||||
|
||||
const handleStopAgent = useCallback(async () => {
|
||||
console.log('[PREVIEW] Stopping agent...');
|
||||
setAgentStatus('idle');
|
||||
await stopStreaming();
|
||||
|
||||
if (agentRunId) {
|
||||
try {
|
||||
await stopAgentMutation.mutateAsync(agentRunId);
|
||||
} catch (error) {
|
||||
console.error('[PREVIEW] Error stopping agent:', error);
|
||||
}
|
||||
}
|
||||
}, [stopStreaming, agentRunId, stopAgentMutation]);
|
||||
|
||||
const handleToolClick = useCallback((assistantMessageId: string | null, toolName: string) => {
|
||||
console.log('[PREVIEW] Tool clicked:', toolName);
|
||||
toast.info(`Tool: ${toolName} (Preview mode - tool details not available)`);
|
||||
}, []);
|
||||
|
||||
const enabledMCPs = agent.configured_mcps || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
|
||||
<div className="h-full flex flex-col bg-muted dark:bg-muted/30">
|
||||
<div className="flex-shrink-0 flex items-center gap-3 p-8">
|
||||
<div
|
||||
className="h-10 w-10 flex items-center justify-center rounded-lg text-lg"
|
||||
style={{ backgroundColor: color }}
|
||||
>
|
||||
{avatar}
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h3 className="font-semibold">{agent.name || 'Unnamed Agent'}</h3>
|
||||
{agent.description && (
|
||||
<p className="text-sm text-muted-foreground">{agent.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<Badge variant="outline" className="text-xs">Preview Mode</Badge>
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<div className="h-full overflow-y-auto scrollbar-hide">
|
||||
<ThreadContent
|
||||
messages={messages}
|
||||
streamingTextContent={streamingTextContent}
|
||||
streamingToolCall={streamingToolCall}
|
||||
agentStatus={agentStatus}
|
||||
handleToolClick={handleToolClick}
|
||||
handleOpenFileViewer={() => {}}
|
||||
streamHookStatus={streamHookStatus}
|
||||
isPreviewMode={true}
|
||||
/>
|
||||
<div ref={messagesEndRef} />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex-shrink-0">
|
||||
<div className="p-4">
|
||||
<ChatInput
|
||||
ref={chatInputRef}
|
||||
onSubmit={threadId ? handleSubmitMessage : handleSubmitFirstMessage}
|
||||
loading={isSubmitting}
|
||||
placeholder={`Message ${agent.name || 'agent'}...`}
|
||||
value={inputValue}
|
||||
onChange={setInputValue}
|
||||
disabled={isSubmitting}
|
||||
isAgentRunning={agentStatus === 'running' || agentStatus === 'connecting'}
|
||||
onStopAgent={handleStopAgent}
|
||||
agentName={agent.name}
|
||||
hideAttachments={false}
|
||||
bgColor='bg-muted-foreground/10'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
};
|
|
@ -61,7 +61,7 @@ export const AgentToolsConfiguration = ({ tools, onToolsChange }: AgentToolsConf
|
|||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3 max-h-[400px] overflow-y-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
|
||||
<div className="gap-4 grid grid-cols-1 md:grid-cols-2 max-h-[400px] overflow-y-auto scrollbar-thin scrollbar-thumb-zinc-300 dark:scrollbar-thumb-zinc-700 scrollbar-track-transparent">
|
||||
{getFilteredTools().map(([toolName, toolInfo]) => (
|
||||
<div
|
||||
key={toolName}
|
||||
|
|
|
@ -16,7 +16,7 @@ interface Agent {
|
|||
configured_mcps?: Array<{ name: string }>;
|
||||
agentpress_tools?: Record<string, any>;
|
||||
avatar?: string;
|
||||
color?: string;
|
||||
avatar_color?: string;
|
||||
}
|
||||
|
||||
interface AgentsGridProps {
|
||||
|
@ -28,7 +28,17 @@ interface AgentsGridProps {
|
|||
}
|
||||
|
||||
const AgentModal = ({ agent, isOpen, onClose, onCustomize, onChat }) => {
|
||||
const { avatar, color } = getAgentAvatar(agent.agent_id);
|
||||
const getAgentStyling = (agent: Agent) => {
|
||||
if (agent.avatar && agent.avatar_color) {
|
||||
return {
|
||||
avatar: agent.avatar,
|
||||
color: agent.avatar_color,
|
||||
};
|
||||
}
|
||||
return getAgentAvatar(agent.agent_id);
|
||||
};
|
||||
|
||||
const { avatar, color } = getAgentStyling(agent);
|
||||
|
||||
const truncateDescription = (text, maxLength = 120) => {
|
||||
if (!text || text.length <= maxLength) return text || 'Try out this agent';
|
||||
|
@ -40,9 +50,9 @@ const AgentModal = ({ agent, isOpen, onClose, onCustomize, onChat }) => {
|
|||
<DialogContent className="max-w-md p-0 overflow-hidden border-none">
|
||||
<DialogTitle className="sr-only">Agent actions</DialogTitle>
|
||||
<div className="relative">
|
||||
<div className={`${color} h-32 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100`}>
|
||||
<div className={`h-32 flex items-center justify-center relative bg-gradient-to-br from-opacity-90 to-opacity-100`} style={{ backgroundColor: color }}>
|
||||
<div className="text-6xl drop-shadow-sm">
|
||||
{agent.avatar || avatar}
|
||||
{avatar}
|
||||
</div>
|
||||
{agent.is_default && (
|
||||
<div className="absolute top-4 right-4">
|
||||
|
@ -109,11 +119,21 @@ export const AgentsGrid = ({
|
|||
setSelectedAgent(null);
|
||||
};
|
||||
|
||||
const getAgentStyling = (agent: Agent) => {
|
||||
if (agent.avatar && agent.avatar_color) {
|
||||
return {
|
||||
avatar: agent.avatar,
|
||||
color: agent.avatar_color,
|
||||
};
|
||||
}
|
||||
return getAgentAvatar(agent.agent_id);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
|
||||
{agents.map((agent) => {
|
||||
const { avatar, color } = getAgentAvatar(agent.agent_id);
|
||||
const { avatar, color } = getAgentStyling(agent);
|
||||
|
||||
return (
|
||||
<div
|
||||
|
@ -121,9 +141,9 @@ export const AgentsGrid = ({
|
|||
className="bg-neutral-100 dark:bg-sidebar border border-border rounded-2xl overflow-hidden hover:bg-muted/50 transition-all duration-200 cursor-pointer group"
|
||||
onClick={() => handleAgentClick(agent)}
|
||||
>
|
||||
<div className={`${color} h-50 flex items-center justify-center relative`}>
|
||||
<div className={`h-50 flex items-center justify-center relative`} style={{ backgroundColor: color }}>
|
||||
<div className="text-4xl">
|
||||
{agent.avatar || avatar}
|
||||
{avatar}
|
||||
</div>
|
||||
{agent.is_default && (
|
||||
<div className="absolute top-3 right-3">
|
||||
|
|
File diff suppressed because it is too large
Load Diff
|
@ -1,6 +1,6 @@
|
|||
export const getAgentAvatar = (agentId: string) => {
|
||||
const avatars = ['🤖', '🎯', '⚡', '🚀', '🔮', '🎨', '📊', '🔧', '💡', '🌟'];
|
||||
const colors = ['bg-cyan-400', 'bg-green-400', 'bg-purple-400', 'bg-blue-400', 'bg-pink-400', 'bg-yellow-400', 'bg-red-400', 'bg-indigo-400'];
|
||||
const colors = ['#06b6d4', '#22c55e', '#8b5cf6', '#3b82f6', '#ec4899', '#eab308', '#ef4444', '#6366f1'];
|
||||
const avatarIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % avatars.length;
|
||||
const colorIndex = agentId.split('').reduce((acc, char) => acc + char.charCodeAt(0), 0) % colors.length;
|
||||
return {
|
||||
|
|
|
@ -14,6 +14,22 @@ import { AgentPreview } from '../../_components/agent-preview';
|
|||
import { cn } from '@/lib/utils';
|
||||
import { getAgentAvatar } from '../../_utils/get-agent-style';
|
||||
import { EditableText } from '@/components/ui/editable';
|
||||
import { StylePicker } from '../../_components/style-picker';
|
||||
|
||||
// Extended agent interface for styling
|
||||
interface AgentWithStyling {
|
||||
agent_id: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
system_prompt?: string;
|
||||
agentpress_tools?: Record<string, { enabled: boolean; description: string }>;
|
||||
configured_mcps?: Array<{ name: string; qualifiedName: string; config: any; enabledTools?: string[] }>;
|
||||
is_default?: boolean;
|
||||
avatar?: string;
|
||||
avatar_color?: string;
|
||||
created_at?: string;
|
||||
updated_at?: string;
|
||||
}
|
||||
|
||||
type SaveStatus = 'idle' | 'saving' | 'saved' | 'error';
|
||||
|
||||
|
@ -33,6 +49,8 @@ export default function AgentConfigurationPage() {
|
|||
agentpress_tools: {},
|
||||
configured_mcps: [],
|
||||
is_default: false,
|
||||
avatar: '',
|
||||
avatar_color: '',
|
||||
});
|
||||
|
||||
const originalDataRef = useRef<typeof formData | null>(null);
|
||||
|
@ -42,13 +60,16 @@ export default function AgentConfigurationPage() {
|
|||
|
||||
useEffect(() => {
|
||||
if (agent) {
|
||||
const agentData = agent as any; // Safe casting for extended properties
|
||||
const initialData = {
|
||||
name: agent.name || '',
|
||||
description: agent.description || '',
|
||||
system_prompt: agent.system_prompt || '',
|
||||
agentpress_tools: agent.agentpress_tools || {},
|
||||
configured_mcps: agent.configured_mcps || [],
|
||||
is_default: agent.is_default || false,
|
||||
name: agentData.name || '',
|
||||
description: agentData.description || '',
|
||||
system_prompt: agentData.system_prompt || '',
|
||||
agentpress_tools: agentData.agentpress_tools || {},
|
||||
configured_mcps: agentData.configured_mcps || [],
|
||||
is_default: agentData.is_default || false,
|
||||
avatar: agentData.avatar || '',
|
||||
avatar_color: agentData.avatar_color || '',
|
||||
};
|
||||
setFormData(initialData);
|
||||
originalDataRef.current = { ...initialData };
|
||||
|
@ -128,6 +149,29 @@ export default function AgentConfigurationPage() {
|
|||
}
|
||||
}, []);
|
||||
|
||||
const handleStyleChange = useCallback((emoji: string, color: string) => {
|
||||
const newFormData = {
|
||||
...formData,
|
||||
avatar: emoji,
|
||||
avatar_color: color,
|
||||
};
|
||||
setFormData(newFormData);
|
||||
debouncedSave(newFormData);
|
||||
}, [formData, debouncedSave]);
|
||||
|
||||
// Get current style with fallback to generated defaults
|
||||
const getCurrentStyle = useCallback(() => {
|
||||
if (formData.avatar && formData.avatar_color) {
|
||||
return {
|
||||
avatar: formData.avatar,
|
||||
color: formData.avatar_color,
|
||||
};
|
||||
}
|
||||
return getAgentAvatar(agentId);
|
||||
}, [formData.avatar, formData.avatar_color, agentId]);
|
||||
|
||||
const currentStyle = getCurrentStyle();
|
||||
|
||||
const getSaveStatusBadge = () => {
|
||||
const showSaved = saveStatus === 'idle' && !hasDataChanged(formData, originalDataRef.current);
|
||||
switch (saveStatus) {
|
||||
|
@ -212,9 +256,19 @@ export default function AgentConfigurationPage() {
|
|||
</div>
|
||||
|
||||
<div className='flex items-center'>
|
||||
<div className={cn(color, 'h-16 w-16 flex items-center justify-center rounded-md text-2xl')}>
|
||||
{avatar}
|
||||
</div>
|
||||
<StylePicker
|
||||
agentId={agentId}
|
||||
currentEmoji={currentStyle.avatar}
|
||||
currentColor={currentStyle.color}
|
||||
onStyleChange={handleStyleChange}
|
||||
>
|
||||
<div
|
||||
className="h-16 w-16 flex items-center justify-center rounded-2xl text-2xl cursor-pointer hover:opacity-80 transition-opacity"
|
||||
style={{ backgroundColor: currentStyle.color }}
|
||||
>
|
||||
{currentStyle.avatar}
|
||||
</div>
|
||||
</StylePicker>
|
||||
<div className='flex flex-col ml-3'>
|
||||
<EditableText
|
||||
value={formData.name}
|
||||
|
@ -285,14 +339,8 @@ export default function AgentConfigurationPage() {
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div className="w-1/2 bg-muted/30 overflow-y-auto">
|
||||
<div className="p-6">
|
||||
<div className="flex items-center gap-2 mb-4">
|
||||
<MessageSquare className="h-5 w-5" />
|
||||
<h2 className="text-lg font-semibold">Preview</h2>
|
||||
</div>
|
||||
<AgentPreview agent={{ ...agent, ...formData }} />
|
||||
</div>
|
||||
<div className="w-1/2 overflow-y-auto">
|
||||
<AgentPreview agent={{ ...agent, ...formData }} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -43,6 +43,7 @@ export interface ChatInputProps {
|
|||
onAgentSelect?: (agentId: string | undefined) => void;
|
||||
agentName?: string;
|
||||
messages?: any[];
|
||||
bgColor?: string;
|
||||
}
|
||||
|
||||
export interface UploadedFile {
|
||||
|
@ -72,6 +73,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
|||
onAgentSelect,
|
||||
agentName,
|
||||
messages = [],
|
||||
bgColor = 'bg-sidebar',
|
||||
},
|
||||
ref,
|
||||
) => {
|
||||
|
@ -237,7 +239,7 @@ export const ChatInput = forwardRef<ChatInputHandles, ChatInputProps>(
|
|||
}}
|
||||
>
|
||||
<div className="w-full text-sm flex flex-col justify-between items-start rounded-lg">
|
||||
<CardContent className="w-full p-1.5 pb-2 bg-sidebar rounded-2xl border">
|
||||
<CardContent className={`w-full p-1.5 pb-2 ${bgColor} rounded-2xl border`}>
|
||||
{onAgentSelect && (
|
||||
<div className="mb-2 px-2">
|
||||
<AgentSelector
|
||||
|
|
|
@ -255,6 +255,7 @@ export interface ThreadContentProps {
|
|||
sandboxId?: string; // Add sandboxId prop
|
||||
project?: Project; // Add project prop
|
||||
debugMode?: boolean; // Add debug mode parameter
|
||||
isPreviewMode?: boolean;
|
||||
}
|
||||
|
||||
export const ThreadContent: React.FC<ThreadContentProps> = ({
|
||||
|
@ -272,7 +273,8 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
streamHookStatus = "idle",
|
||||
sandboxId,
|
||||
project,
|
||||
debugMode = false
|
||||
debugMode = false,
|
||||
isPreviewMode = false,
|
||||
}) => {
|
||||
const messagesEndRef = useRef<HTMLDivElement>(null);
|
||||
const messagesContainerRef = useRef<HTMLDivElement>(null);
|
||||
|
@ -284,6 +286,10 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
// React Query file preloader
|
||||
const { preloadFiles } = useFilePreloader();
|
||||
|
||||
const containerClassName = isPreviewMode
|
||||
? "flex-1 overflow-y-auto scrollbar-thin scrollbar-track-secondary/0 scrollbar-thumb-primary/10 scrollbar-thumb-rounded-full hover:scrollbar-thumb-primary/10 px-6 py-4 pb-72"
|
||||
: "flex-1 overflow-y-auto scrollbar-thin scrollbar-track-secondary/0 scrollbar-thumb-primary/10 scrollbar-thumb-rounded-full hover:scrollbar-thumb-primary/10 px-6 py-4 pb-72 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60";
|
||||
|
||||
// In playback mode, we use visibleMessages instead of messages
|
||||
const displayMessages = readOnly && visibleMessages ? visibleMessages : messages;
|
||||
|
||||
|
@ -339,7 +345,7 @@ export const ThreadContent: React.FC<ThreadContentProps> = ({
|
|||
|
||||
<div
|
||||
ref={messagesContainerRef}
|
||||
className="flex-1 overflow-y-auto scrollbar-thin scrollbar-track-secondary/0 scrollbar-thumb-primary/10 scrollbar-thumb-rounded-full hover:scrollbar-thumb-primary/10 px-6 py-4 pb-72 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60"
|
||||
className={containerClassName}
|
||||
onScroll={handleScroll}
|
||||
>
|
||||
<div className="mx-auto max-w-3xl">
|
||||
|
|
Loading…
Reference in New Issue