CoolFace
Apppublic

garvitsachdeva/SpindleFlow-RL

sourceHugging Faceupdated 5mo agoView on Hugging Face
0likes
orchestrator_widget.py561 linesDownload Raw Back to demo
1"""2Animated robot orchestrator widget for the SpindleFlow RL demo.3Exports one public function: render_orchestrator(state, height=600)4 5All HTML/CSS/JS is self-contained — no CDN, no external calls.6Safe for Hugging Face Spaces iframe sandbox.7"""8 9from __future__ import annotations10import json11import math12 13# ── Agent color and icon maps ─────────────────────────────────────────────────14 15SPEC_COLORS = {16    "frontend_react":     "#00d4ff",17    "backend_api":        "#7c3aed",18    "database_architect": "#f59e0b",19    "devops_engineer":    "#10b981",20    "security_analyst":   "#ef4444",21    "product_strategist": "#8b5cf6",22    "ux_designer":        "#ec4899",23    "tech_writer":        "#94a3b8",24}25 26SPEC_ICONS = {27    "frontend_react":     "FE",28    "backend_api":        "API",29    "database_architect": "DB",30    "devops_engineer":    "OPS",31    "security_analyst":   "SEC",32    "product_strategist": "PM",33    "ux_designer":        "UX",34    "tech_writer":        "DOC",35}36 37_SPAWNED_COLOR   = "#fbbf24"   # gold for auto-spawned agents38_FALLBACK_COLORS = [           # cycle through for multiple unknown agents39    "#fbbf24", "#f472b6", "#34d399", "#fb923c", "#a78bfa",40]41 42 43def _agent_color(agent_id: str, spawned_ids: set) -> str:44    if agent_id in SPEC_COLORS:45        return SPEC_COLORS[agent_id]46    if agent_id in spawned_ids:47        return _SPAWNED_COLOR48    # deterministic fallback based on hash49    return _FALLBACK_COLORS[hash(agent_id) % len(_FALLBACK_COLORS)]50 51 52def _agent_icon(agent_id: str, spawned_ids: set) -> str:53    if agent_id in SPEC_ICONS:54        return SPEC_ICONS[agent_id]55    if agent_id in spawned_ids:56        return "⚡"57    return agent_id[:3].upper()58 59 60# ── Layout ────────────────────────────────────────────────────────────────────61 62def _agent_positions(agent_ids: list,63                     canvas_w: int = 780,64                     canvas_h: int = 560) -> dict:65    """Return {agent_id: (x, y)} in a straight vertical column on the right."""66    col_x = canvas_w - 11567    n = len(agent_ids)68    if n == 0:69        return {}70    pad_top = 5071    pad_bot = 5072    usable  = canvas_h - pad_top - pad_bot73    step    = usable / n74    positions = {}75    for i, aid in enumerate(agent_ids):76        y = round(pad_top + step * i + step / 2)77        positions[aid] = (col_x, y)78    return positions79 80 81# ── SVG builders ──────────────────────────────────────────────────────────────82 83def _robot_svg() -> str:84    return """85    <g id="robot" transform="translate(160, 280)">86 87      <!-- Antenna -->88      <line x1="0" y1="-115" x2="0" y2="-95" stroke="#00d4ff" stroke-width="2"/>89      <circle cx="0" cy="-120" r="5" fill="#00d4ff" class="antenna-pulse"/>90 91      <!-- Head -->92      <rect x="-38" y="-95" width="76" height="62" rx="10"93            fill="#0d1117" stroke="#00d4ff" stroke-width="1.5"94            class="head-glow"/>95 96      <!-- Left Eye -->97      <circle cx="-14" cy="-68" r="10" fill="#001a2e"/>98      <circle cx="-14" cy="-68" r="6" fill="#00d4ff" class="eye-left"/>99      <circle cx="-11" cy="-71" r="2" fill="white" opacity="0.6"/>100 101      <!-- Right Eye -->102      <circle cx="14" cy="-68" r="10" fill="#001a2e"/>103      <circle cx="14" cy="-68" r="6" fill="#00d4ff" class="eye-right"/>104      <circle cx="17" cy="-71" r="2" fill="white" opacity="0.6"/>105 106      <!-- Mouth -->107      <path d="M -14 -46 Q 0 -38 14 -46"108            fill="none" stroke="#00d4ff" stroke-width="2"109            stroke-linecap="round" class="mouth"/>110 111      <!-- Neck -->112      <rect x="-8" y="-33" width="16" height="10" rx="3"113            fill="#0d1117" stroke="#1a2a3a" stroke-width="1"/>114 115      <!-- Body -->116      <rect x="-45" y="-23" width="90" height="80" rx="12"117            fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1.5"/>118 119      <!-- Core (spinning hexagon) -->120      <g class="core-spin" transform="translate(0, 17)">121        <polygon points="0,-18 15.6,-9 15.6,9 0,18 -15.6,9 -15.6,-9"122                 fill="none" stroke="#00d4ff" stroke-width="1.5" opacity="0.8"/>123        <polygon points="0,-11 9.5,-5.5 9.5,5.5 0,11 -9.5,5.5 -9.5,-5.5"124                 fill="rgba(0,212,255,0.15)" stroke="#00d4ff" stroke-width="1"/>125        <circle cx="0" cy="0" r="4" fill="#00d4ff" class="core-pulse"/>126      </g>127 128      <!-- Left Arm -->129      <g id="arm-left">130        <rect x="-68" y="-18" width="24" height="12" rx="6"131              fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1.5"/>132        <rect x="-72" y="-8" width="14" height="28" rx="7"133              fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1.5"/>134      </g>135 136      <!-- Right Arm -->137      <g id="arm-right" class="arm-idle">138        <rect x="44" y="-18" width="24" height="12" rx="6"139              fill="#0a0f1a" stroke="#00d4ff" stroke-width="1.5"/>140        <rect x="58" y="-8" width="14" height="28" rx="7"141              fill="#0a0f1a" stroke="#00d4ff" stroke-width="1.5"/>142        <circle cx="65" cy="22" r="5" fill="#00d4ff" class="hand-glow"/>143      </g>144 145      <!-- Legs -->146      <rect x="-28" y="57" width="18" height="28" rx="6"147            fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1.5"/>148      <rect x="10" y="57" width="18" height="28" rx="6"149            fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1.5"/>150 151      <!-- Feet -->152      <ellipse cx="-19" cy="87" rx="16" ry="7"153               fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1"/>154      <ellipse cx="19" cy="87" rx="16" ry="7"155               fill="#0a0f1a" stroke="#1a3a5a" stroke-width="1"/>156 157      <!-- Shadow -->158      <ellipse cx="0" cy="97" rx="50" ry="8"159               fill="rgba(0,212,255,0.05)"/>160    </g>161    """162 163 164def _agent_card_svg(agent_id: str, x: int, y: int,165                    status: str, color: str,166                    is_spawned: bool = False) -> str:167    """Returns SVG <g> for one agent card. status: idle | active | done."""168    icon  = SPEC_ICONS.get(agent_id, ("⚡" if is_spawned else agent_id[:3].upper()))169    label = agent_id.replace("_", " ").title()170    label = label[:18] + ("…" if len(label) > 18 else "")171 172    status_class = {"idle": "agent-idle", "active": "agent-active",173                    "done": "agent-done"}.get(status, "agent-idle")174    opacity = "1.0" if status != "idle" else "0.40"175    border  = "#fbbf24" if is_spawned else color176    spawn_star = (177        f'<text x="26" y="-26" text-anchor="middle" font-size="10" fill="#fbbf24">⚡</text>'178        if is_spawned else ""179    )180 181    return f"""182    <g class="agent-card {status_class}" transform="translate({x},{y})"183       id="agent-{agent_id}" opacity="{opacity}">184      <circle cx="0" cy="0" r="36" fill="none"185              stroke="{border}" stroke-width="1.5"186              class="agent-ring" opacity="0.25"/>187      <rect x="-26" y="-26" width="52" height="52" rx="10"188            fill="#0a0f1a" stroke="{border}" stroke-width="1.5"189            opacity="0.95"/>190      <text x="0" y="5" text-anchor="middle" dominant-baseline="middle"191            fill="{color}" font-family="'JetBrains Mono', monospace"192            font-size="11" font-weight="700">{icon}</text>193      {spawn_star}194      <circle cx="20" cy="-20" r="5" fill="{color}" class="status-dot"/>195      <text x="0" y="40" text-anchor="middle"196            fill="#64748b" font-family="system-ui, sans-serif"197            font-size="8.5" letter-spacing="0.3">{label}</text>198      <g class="done-check" opacity="0">199        <circle cx="20" cy="-20" r="7" fill="#10b981"/>200        <text x="20" y="-16" text-anchor="middle" fill="white" font-size="9">✓</text>201      </g>202    </g>203    """204 205 206def _beam_svg(edges: list, agent_positions: dict) -> str:207    """Returns SVG beam lines for all current delegation edges."""208    robot_hand_x, robot_hand_y = 225, 302209    lines = []210    for caller, callee in edges:211        if callee not in agent_positions:212            continue213        tx, ty = agent_positions[callee]214        color  = SPEC_COLORS.get(callee, _SPAWNED_COLOR)215        lines.append(f"""216        <line id="beam-{callee}"217              x1="{robot_hand_x}" y1="{robot_hand_y}" x2="{tx}" y2="{ty}"218              stroke="{color}" stroke-width="1.5" stroke-linecap="round"219              opacity="0.55" stroke-dasharray="6 4" class="beam-line beam-animate"/>220        <circle id="dot-{callee}" r="4" fill="{color}" opacity="0.9" class="beam-dot">221          <animateMotion dur="0.9s" repeatCount="indefinite"222                         path="M {robot_hand_x},{robot_hand_y} L {tx},{ty}"/>223        </circle>224        <circle id="burst-{callee}" cx="{tx}" cy="{ty}" r="8"225                fill="none" stroke="{color}" stroke-width="2"226                opacity="0" class="burst-ring burst-animate"/>227        """)228    return "\n".join(lines)229 230 231# ── HTML template ─────────────────────────────────────────────────────────────232 233def _html_template(*, agents_svg, beams_svg, robot_svg, state_json,234                   task_short, reward_html, step, phase, mode, mode_color) -> str:235    return f"""<!DOCTYPE html>236<html>237<head>238<meta charset="utf-8"/>239<style>240  * {{ box-sizing: border-box; margin: 0; padding: 0; }}241  body {{ background: transparent; font-family: 'JetBrains Mono', 'Fira Code', monospace; overflow: hidden; }}242 243  .canvas-wrap {{244    position: relative; width: 100%; height: 560px;245    background: radial-gradient(ellipse at 25% 50%, rgba(0,212,255,0.04) 0%, transparent 60%),246                radial-gradient(ellipse at 85% 50%, rgba(124,58,237,0.03) 0%, transparent 50%),247                #080d14;248    border-radius: 16px; border: 1px solid rgba(0,212,255,0.1); overflow: hidden;249  }}250  .canvas-wrap::before {{251    content: ''; position: absolute; inset: 0;252    background-image: linear-gradient(rgba(0,212,255,0.025) 1px, transparent 1px),253                      linear-gradient(90deg, rgba(0,212,255,0.025) 1px, transparent 1px);254    background-size: 40px 40px; border-radius: 16px; pointer-events: none;255  }}256  svg.main-svg {{ position: absolute; top: 0; left: 0; width: 100%; height: 100%; }}257 258  .info-bar {{259    position: absolute; bottom: 0; left: 0; right: 0; height: 44px;260    background: rgba(0,0,0,0.5); border-top: 1px solid rgba(255,255,255,0.05);261    border-radius: 0 0 16px 16px; display: flex; align-items: center;262    padding: 0 20px; gap: 24px; font-size: 11px; color: #475569;263  }}264  .info-badge {{ display: flex; align-items: center; gap: 6px; }}265  .info-badge .label {{ font-size: 9px; text-transform: uppercase; letter-spacing: 1px; color: #334155; }}266  .info-badge .value {{ font-weight: 700; color: #94a3b8; }}267  .task-text {{ flex: 1; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; color: #475569; font-size: 10px; }}268 269  .orch-label   {{ position: absolute; top: 18px; left: 18px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #00d4ff; opacity: 0.7; }}270  .agents-label {{ position: absolute; top: 18px; right: 18px; font-size: 9px; font-weight: 700; text-transform: uppercase; letter-spacing: 2px; color: #475569; opacity: 0.7; }}271 272  .divider-line {{273    position: absolute; left: 47%; top: 8%; height: 84%; width: 1px;274    background: linear-gradient(to bottom, transparent, rgba(0,212,255,0.12), transparent);275  }}276 277  /* Robot animations */278  @keyframes antenna-blink {{ 0%,90%,100% {{ opacity:1; }} 95% {{ opacity:0.2; }} }}279  .antenna-pulse {{ animation: antenna-blink 2.5s ease-in-out infinite; }}280 281  @keyframes core-rotation {{ from {{ transform: rotate(0deg); }} to {{ transform: rotate(360deg); }} }}282  .core-spin {{ transform-origin: 0px 17px; animation: core-rotation 4s linear infinite; }}283 284  @keyframes core-pulse {{ 0%,100% {{ opacity:0.8; r:4px; }} 50% {{ opacity:1; r:6px; fill:white; }} }}285  .core-pulse {{ animation: core-pulse 1.5s ease-in-out infinite; }}286 287  @keyframes eye-blink {{ 0%,92%,100% {{ ry:6px; }} 96% {{ ry:1px; }} }}288  .eye-left, .eye-right {{ animation: eye-blink 4s ease-in-out infinite; transform-box: fill-box; transform-origin: center; }}289 290  @keyframes hand-glow {{ 0%,100% {{ opacity:0.6; r:5px; }} 50% {{ opacity:1; r:8px; }} }}291  .hand-glow {{ animation: hand-glow 1.2s ease-in-out infinite; }}292 293  @keyframes head-glow-pulse {{ 0%,100% {{ filter: drop-shadow(0 0 4px rgba(0,212,255,0.3)); }} 50% {{ filter: drop-shadow(0 0 12px rgba(0,212,255,0.7)); }} }}294  .head-glow {{ animation: head-glow-pulse 2s ease-in-out infinite; }}295 296  @keyframes arm-extend {{ 0% {{ transform: rotate(0deg) translateX(0px); }} 100% {{ transform: rotate(-15deg) translateX(12px); }} }}297  .arm-delegating {{ transform-origin: 55px 0px; animation: arm-extend 0.4s ease-out forwards; }}298 299  /* Agent animations */300  @keyframes agent-active-pulse {{ 0%,100% {{ filter: drop-shadow(0 0 6px currentColor); }} 50% {{ filter: drop-shadow(0 0 18px currentColor); }} }}301  .agent-active {{ animation: agent-active-pulse 0.8s ease-in-out infinite; opacity: 1 !important; }}302  .agent-done {{ opacity: 1 !important; }}303  .agent-done .status-dot {{ fill: #10b981 !important; }}304  .agent-done .done-check {{ opacity: 1 !important; }}305 306  @keyframes ring-expand {{ from {{ r:28px; opacity:0.6; }} to {{ r:48px; opacity:0; }} }}307  .agent-active .agent-ring {{ animation: ring-expand 1s ease-out infinite; }}308 309  /* Beam animations */310  @keyframes beam-draw {{ from {{ stroke-dashoffset:200; opacity:0; }} to {{ stroke-dashoffset:0; opacity:0.55; }} }}311  .beam-animate {{ stroke-dasharray: 6 4; animation: beam-draw 0.4s ease-out forwards; }}312 313  @keyframes burst-expand {{ 0% {{ r:8px; opacity:0.9; stroke-width:3px; }} 100% {{ r:28px; opacity:0; stroke-width:1px; }} }}314  .burst-animate {{ animation: burst-expand 0.6s ease-out infinite; }}315 316  .robot-thinking .core-spin {{ animation-duration: 1.2s !important; }}317  .robot-thinking .antenna-pulse {{ animation: antenna-blink 0.6s ease-in-out infinite !important; }}318 319  /* Sequential reveal */320  @keyframes slide-in-right {{321    from {{ opacity: 0; transform: translateX(22px); }}322    to   {{ opacity: 1; transform: translateX(0); }}323  }}324 325  #particles {{ position: absolute; top: 0; left: 0; width: 100%; height: 560px; pointer-events: none; }}326</style>327</head>328<body>329<div class="canvas-wrap" id="canvas-wrap">330  <canvas id="particles"></canvas>331  <div class="orch-label">Orchestrator</div>332  <div class="agents-label">Specialists</div>333  <div class="divider-line"></div>334 335  <svg class="main-svg" viewBox="0 0 780 560" xmlns="http://www.w3.org/2000/svg">336    <g id="beams-layer">{beams_svg}</g>337    <g id="agents-layer">{agents_svg}</g>338    <g id="robot-layer">{robot_svg}</g>339  </svg>340 341  <div class="info-bar">342    <div class="info-badge">343      <span class="label">Step</span>344      <span class="value">{step}</span>345    </div>346    <div class="info-badge">347      <span class="label">Phase</span>348      <span class="value">{phase}</span>349    </div>350    <div class="info-badge">351      <span class="label">Mode</span>352      <span class="value" style="color:{mode_color};">{mode}</span>353    </div>354    <div class="info-badge">355      <span class="label">Reward</span>356      <span class="value">{reward_html}</span>357    </div>358    <div class="task-text" title="{task_short}">{task_short}</div>359  </div>360</div>361 362<script>363const STATE = {state_json};364 365const robotLayer = document.getElementById('robot-layer');366const armRight   = document.getElementById('arm-right');367 368if (STATE.robot_state === 'thinking' || STATE.robot_state === 'delegating') {{369  robotLayer.classList.add('robot-thinking');370}}371if (STATE.robot_state === 'delegating' && armRight) {{372  armRight.classList.remove('arm-idle');373  armRight.classList.add('arm-delegating');374}}375 376// Sequential reveal: agents appear one-by-one with staggered delays377if (STATE.mode === 'SEQUENTIAL' && !STATE.done && STATE.called.length > 0) {{378  STATE.called.forEach(function(agentId, idx) {{379    var el = document.getElementById('agent-' + agentId);380    if (!el) return;381    el.style.opacity = '0';382    (function(element, delay) {{383      setTimeout(function() {{384        element.style.transition = 'opacity 0.5s ease';385        element.style.opacity = '1';386      }}, delay);387    }})(el, 250 + idx * 650);388  }});389}}390 391function spawnParticles(x, y, color) {{392  const canvas = document.getElementById('particles');393  if (!canvas) return;394  const ctx = canvas.getContext('2d');395  canvas.width  = canvas.offsetWidth;396  canvas.height = canvas.offsetHeight;397  const particles = [];398  for (let i = 0; i < 18; i++) {{399    const angle = (Math.PI * 2 * i) / 18;400    const speed = 1.5 + Math.random() * 2.5;401    particles.push({{ x, y, vx: Math.cos(angle)*speed, vy: Math.sin(angle)*speed, life: 1.0, r: 2+Math.random()*2, color }});402  }}403  function animate() {{404    ctx.clearRect(0, 0, canvas.width, canvas.height);405    let alive = false;406    particles.forEach(p => {{407      if (p.life <= 0) return;408      p.x += p.vx; p.y += p.vy; p.vx *= 0.92; p.vy *= 0.92; p.life -= 0.025; alive = true;409      ctx.beginPath(); ctx.arc(p.x, p.y, p.r, 0, Math.PI*2);410      ctx.fillStyle = color + Math.floor(p.life*255).toString(16).padStart(2,'0');411      ctx.fill();412    }});413    if (alive) requestAnimationFrame(animate);414    else ctx.clearRect(0, 0, canvas.width, canvas.height);415  }}416  animate();417}}418 419if (STATE.active) {{420  const activeEl = document.getElementById('agent-' + STATE.active);421  if (activeEl) {{422    const wrap  = document.getElementById('canvas-wrap');423    const wRect = wrap.getBoundingClientRect();424    const ct    = activeEl.getCTM();425    if (ct) {{426      const scaleX = wRect.width  / 780;427      const scaleY = wRect.height / 560;428      const tx = ct.e * scaleX;429      const ty = ct.f * scaleY;430      const rect = activeEl.querySelector('rect');431      const agentColor = rect ? rect.getAttribute('stroke') : '#00d4ff';432      setTimeout(() => spawnParticles(tx, ty, agentColor), 300);433    }}434  }}435}}436 437(function breathe() {{438  const robot = document.getElementById('robot');439  if (!robot) return;440  let t = 0;441  function frame() {{442    t += 0.02;443    const dy = Math.sin(t) * 2.5;444    robot.setAttribute('transform', `translate(160, ${{280 + dy}})`);445    requestAnimationFrame(frame);446  }}447  frame();448}})();449</script>450</body>451</html>"""452 453 454# ── State assembler ───────────────────────────────────────────────────────────455 456def _build_html(state: dict) -> str:457    called      = state.get("called", [])458    active      = state.get("active", "")459    edges       = state.get("edges", [])460    task        = state.get("task", "")461    step        = state.get("step", 0)462    mode        = state.get("mode", "SEQUENTIAL")463    done        = state.get("done", False)464    reward      = state.get("reward", None)465    phase       = state.get("phase", 1)466    spawned_ids = set(state.get("spawned", []))467 468    # Show only agents that were actually called (+ active if mid-step)469    all_agents = list(called)470    if active and active not in all_agents:471        all_agents.append(active)472 473    # Nothing delegated yet — robot is idle/thinking, no agent cards needed474    positions = _agent_positions(all_agents) if all_agents else {}475 476    def agent_status(aid):477        if aid == active: return "active"478        if aid in called: return "done"479        return "idle"480 481    agents_svg = "\n".join(482        _agent_card_svg(483            aid, *positions[aid],484            agent_status(aid),485            _agent_color(aid, spawned_ids),486            is_spawned=(aid in spawned_ids),487        )488        for aid in all_agents489    )490    beams_svg = _beam_svg(edges, positions)491    robot_svg = _robot_svg()492 493    robot_state = (494        "delegating" if active else495        "done"       if done   else496        "thinking"   if step > 0 else497        "idle"498    )499 500    task_short = (task[:72] + "…") if len(task) > 72 else task501 502    if reward is not None:503        sign         = "+" if reward >= 0 else ""504        reward_color = "#10b981" if reward >= 0 else "#ef4444"505        reward_html  = f'<span style="color:{reward_color};font-weight:700;">{sign}{reward:.3f}</span>'506    else:507        reward_html  = '<span style="color:#334155;">—</span>'508 509    mode_color = {510        "SEQUENTIAL":     "#00d4ff",511        "PARALLEL":       "#7c3aed",512        "FAN_OUT_REDUCE": "#f59e0b",513        "ITERATIVE":      "#10b981",514        "STOP":           "#ef4444",515    }.get(mode, "#64748b")516 517    state_json = json.dumps({518        "robot_state": robot_state,519        "active":      active,520        "called":      called,521        "step":        step,522        "done":        done,523        "mode":        mode,524        "spawned":     list(spawned_ids),525    })526 527    return _html_template(528        agents_svg  = agents_svg,529        beams_svg   = beams_svg,530        robot_svg   = robot_svg,531        state_json  = state_json,532        task_short  = task_short,533        reward_html = reward_html,534        step        = step,535        phase       = phase,536        mode        = mode,537        mode_color  = mode_color,538    )539 540 541# ── Public API ────────────────────────────────────────────────────────────────542 543def render_orchestrator(state: dict, height: int = 600) -> None:544    """545    Render the animated robot orchestrator widget in a Streamlit page.546 547    state keys:548      called   — list of specialist IDs called so far this episode549      active   — specialist being called right now (or "")550      edges    — list of [caller_id, callee_id] pairs551      task     — task description string552      step     — current step number553      mode     — delegation mode name (e.g. "SEQUENTIAL")554      done     — whether the episode is finished555      reward   — cumulative reward float (or None)556      phase    — curriculum phase int557      spawned  — list of auto-spawned specialist IDs (shown in gold)558    """559    import streamlit.components.v1 as components560    components.html(_build_html(state), height=height, scrolling=False)561