/ src / components / org-chart / delegation-bubble.tsx
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  }