delegation-bubble.tsx
1 'use client' 2 3 import ReactMarkdown from 'react-markdown' 4 import remarkGfm from 'remark-gfm' 5 import { AgentAvatar } from '@/components/agents/agent-avatar' 6 7 export interface DelegationBubbleData { 8 senderAgent: { id: string; name: string; avatarSeed?: string; avatarUrl?: string | null } 9 receiverAgent: { id: string; name: string; avatarSeed?: string; avatarUrl?: string | null } 10 task: string | null 11 result: string | null 12 color: 'indigo' | 'emerald' | 'red' 13 timestamp: number 14 } 15 16 interface Props { 17 data: DelegationBubbleData 18 isHoverOnly?: boolean 19 } 20 21 const ACCENT: Record<string, { border: string }> = { 22 indigo: { border: 'rgba(99,102,241,0.25)' }, 23 emerald: { border: 'rgba(52,211,153,0.25)' }, 24 red: { border: 'rgba(244,63,94,0.25)' }, 25 } 26 27 export function DelegationBubble({ data, isHoverOnly }: Props) { 28 const accent = ACCENT[data.color] || ACCENT.indigo 29 30 return ( 31 <div 32 className={isHoverOnly ? '' : 'pointer-events-none'} 33 style={{ 34 width: 280, 35 ...(isHoverOnly 36 ? { opacity: 0.85 } 37 : { animation: 'delegationBubbleFade 5s ease-out forwards' }), 38 }} 39 onWheel={isHoverOnly ? (e) => e.stopPropagation() : undefined} 40 > 41 <div 42 className="rounded-[8px] px-3 py-2 shadow-lg" 43 style={{ 44 background: '#12121e', 45 border: `1px solid ${accent.border}`, 46 maxHeight: 120, 47 overflowY: isHoverOnly ? 'auto' : 'hidden', 48 }} 49 > 50 {/* Sender line — left-aligned */} 51 <div className="flex items-start gap-1.5 mb-1"> 52 <AgentAvatar 53 seed={data.senderAgent.avatarSeed || null} 54 avatarUrl={data.senderAgent.avatarUrl} 55 name={data.senderAgent.name} 56 size={16} 57 /> 58 <div className="min-w-0 flex-1"> 59 <div className="text-[9px] font-semibold text-white/70 mb-0.5">{data.senderAgent.name}</div> 60 <div className="text-[11px] text-white/90 break-words prose prose-invert prose-sm max-w-none [&_p]:m-0 [&_p]:leading-snug [&_ul]:m-0 [&_ol]:m-0 [&_li]:m-0 [&_code]:text-[10px] [&_code]:bg-white/10 [&_code]:px-1 [&_code]:rounded"> 61 <ReactMarkdown remarkPlugins={[remarkGfm]}> 62 {data.task || '...'} 63 </ReactMarkdown> 64 </div> 65 </div> 66 </div> 67 68 {/* Receiver line — right-aligned */} 69 {(data.result || data.color !== 'indigo') && ( 70 <div className="flex items-start gap-1.5 justify-end"> 71 <div className="min-w-0 flex-1 text-right"> 72 <div className="text-[11px] text-white/80 break-words prose prose-invert prose-sm max-w-none [&_p]:m-0 [&_p]:leading-snug [&_ul]:m-0 [&_ol]:m-0 [&_li]:m-0 [&_code]:text-[10px] [&_code]:bg-white/10 [&_code]:px-1 [&_code]:rounded"> 73 <ReactMarkdown remarkPlugins={[remarkGfm]}> 74 {data.result || (data.color === 'emerald' ? 'Done' : 'Failed')} 75 </ReactMarkdown> 76 </div> 77 <div className="text-[9px] font-semibold text-white/70 mt-0.5">{data.receiverAgent.name}</div> 78 </div> 79 <AgentAvatar 80 seed={data.receiverAgent.avatarSeed || null} 81 avatarUrl={data.receiverAgent.avatarUrl} 82 name={data.receiverAgent.name} 83 size={16} 84 /> 85 </div> 86 )} 87 </div> 88 89 {/* Down-pointing caret */} 90 <div className="flex justify-center"> 91 <div 92 className="w-0 h-0" 93 style={{ 94 borderLeft: '6px solid transparent', 95 borderRight: '6px solid transparent', 96 borderTop: `6px solid ${accent.border}`, 97 }} 98 /> 99 </div> 100 </div> 101 ) 102 }