[object Object]

← back to Big Red

feat(widget): avatar launcher with red->green live state on click

09a1667e99397e02c9ab80bf949b34bac41de327 · 2026-05-11 23:52:22 -0700 · Steve Abrams

Replaces the BIG/RED text glyph with Steve's circular avatar headshot
(220px PNG seeded from agent-avatars/steve.png at public/avatar/steve.jpg
— matches the /api/avatar exists path).

Visual state machine:
- Closed: red ring + red-pulse animation + red wash overlay (mix-blend
  multiply) so the avatar reads tinted but recognizable
- Hover: scales 1.08
- Open ('live'): green ring + green-pulse + green wash overlay; tag pill
  next to the launcher swaps from 'TALK TO BIG RED' (red) to 'LIVE — TAP
  TO HIDE' (green). Live indicator dot appears in the iframe title bar.
- Iframe loads /?embed=1&voice=1 so Big Red can default to voice mode

Falls back gracefully to 'BR' monogram if the avatar 404s, so the widget
still renders on cold installs before the user uploads a face.

Files touched

Diff

commit 09a1667e99397e02c9ab80bf949b34bac41de327
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 11 23:52:22 2026 -0700

    feat(widget): avatar launcher with red->green live state on click
    
    Replaces the BIG/RED text glyph with Steve's circular avatar headshot
    (220px PNG seeded from agent-avatars/steve.png at public/avatar/steve.jpg
    — matches the /api/avatar exists path).
    
    Visual state machine:
    - Closed: red ring + red-pulse animation + red wash overlay (mix-blend
      multiply) so the avatar reads tinted but recognizable
    - Hover: scales 1.08
    - Open ('live'): green ring + green-pulse + green wash overlay; tag pill
      next to the launcher swaps from 'TALK TO BIG RED' (red) to 'LIVE — TAP
      TO HIDE' (green). Live indicator dot appears in the iframe title bar.
    - Iframe loads /?embed=1&voice=1 so Big Red can default to voice mode
    
    Falls back gracefully to 'BR' monogram if the avatar 404s, so the widget
    still renders on cold installs before the user uploads a face.
---
 public/avatar/steve.jpg | Bin 0 -> 81865 bytes
 public/widget.js        | 224 ++++++++++++++++++++++++++++++++----------------
 2 files changed, 148 insertions(+), 76 deletions(-)

diff --git a/public/avatar/steve.jpg b/public/avatar/steve.jpg
new file mode 100644
index 0000000..fbc9b84
Binary files /dev/null and b/public/avatar/steve.jpg differ
diff --git a/public/widget.js b/public/widget.js
index 34136d0..83e736d 100644
--- a/public/widget.js
+++ b/public/widget.js
@@ -1,16 +1,13 @@
-// Big Red embed widget — drops a floating launcher into the lower-left of the
-// host page. Click → expands to an iframe of the Big Red chat UI.
+// Big Red embed widget — drops a circular avatar launcher into the lower-left
+// of the host page. Click → expands to an iframe of the Big Red chat UI.
+// The launcher avatar is RED when closed and turns GREEN when open (live chat).
 //
 // Usage on a host page:
 //   <script src="https://<big-red-host>/widget.js"
 //           data-host="https://<big-red-host>"
 //           defer></script>
 //
-// Or, if same-origin / dev:
-//   <script src="/widget.js" data-host="http://localhost:9935" defer></script>
-//
-// The script reads its own `src` to derive the default host if data-host is
-// omitted. Idempotent — safe to load twice; only one launcher is mounted.
+// Idempotent — safe to load twice; only one launcher is mounted.
 (function () {
   if (window.__bigRedMounted) return;
   window.__bigRedMounted = true;
@@ -27,59 +24,109 @@
       left: 22px;
       bottom: 22px;
       z-index: 99990;
-      width: 60px;
-      height: 60px;
+      width: 64px;
+      height: 64px;
       border-radius: 50%;
-      background:
-        radial-gradient(circle at 35% 30%, #ff5560 0%, #d8323a 45%, #8b1419 100%);
-      box-shadow:
-        0 0 0 2px rgba(255,255,255,.08),
-        0 8px 28px rgba(216,50,58,.55),
-        0 2px 10px rgba(0,0,0,.4);
       border: 0;
+      padding: 0;
       cursor: pointer;
-      display: flex;
-      align-items: center;
-      justify-content: center;
-      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
-      font-weight: 900;
-      letter-spacing: 0.06em;
-      color: #fff;
-      font-size: 12px;
-      text-shadow: 0 0 8px rgba(0,0,0,.5);
-      transition: transform .18s cubic-bezier(.2,.7,.2,1), box-shadow .18s, opacity .18s;
-      animation: bigred-pulse 2.6s ease-in-out infinite;
-    }
-    .bigred-launcher:hover {
-      transform: scale(1.08);
+      background: #1a0506;
+      overflow: hidden;
       box-shadow:
-        0 0 0 3px rgba(255,255,255,.14),
-        0 12px 40px rgba(216,50,58,.7),
-        0 2px 12px rgba(0,0,0,.5);
+        0 0 0 3px #d8323a,
+        0 0 0 5px rgba(216,50,58,.18),
+        0 8px 24px rgba(216,50,58,.5),
+        0 2px 10px rgba(0,0,0,.4);
+      transition: transform .18s cubic-bezier(.2,.7,.2,1), box-shadow .25s ease;
+      animation: bigred-pulse-red 2.6s ease-in-out infinite;
+    }
+    .bigred-launcher img {
+      width: 100%; height: 100%; display: block;
+      object-fit: cover; object-position: center 28%;
+      filter: saturate(.6) brightness(.92);
+      transition: filter .25s ease;
     }
-    .bigred-launcher:active { transform: scale(0.96); }
-    .bigred-launcher.open { opacity: 0; pointer-events: none; }
-    .bigred-launcher .label {
+    .bigred-launcher::after {
+      /* red wash overlay (closed state) */
+      content: '';
+      position: absolute; inset: 0;
+      border-radius: 50%;
+      background: radial-gradient(circle at 35% 30%, rgba(255,85,96,.18) 0%, rgba(139,20,25,.42) 65%, rgba(58,10,12,.55) 100%);
+      mix-blend-mode: multiply;
+      transition: opacity .28s ease, background .28s ease;
       pointer-events: none;
-      letter-spacing: 0.14em;
     }
-    @keyframes bigred-pulse {
+    .bigred-launcher:hover { transform: scale(1.08); }
+    .bigred-launcher:active { transform: scale(0.94); }
+
+    .bigred-launcher.live {
+      box-shadow:
+        0 0 0 3px #2fbf4a,
+        0 0 0 5px rgba(47,191,74,.20),
+        0 8px 28px rgba(47,191,74,.55),
+        0 2px 10px rgba(0,0,0,.4);
+      animation: bigred-pulse-green 1.8s ease-in-out infinite;
+    }
+    .bigred-launcher.live img { filter: saturate(.85) brightness(1); }
+    .bigred-launcher.live::after {
+      background: radial-gradient(circle at 35% 30%, rgba(120,255,140,.22) 0%, rgba(40,160,70,.30) 65%, rgba(20,80,40,.42) 100%);
+    }
+
+    @keyframes bigred-pulse-red {
       0%, 100% { box-shadow:
-        0 0 0 2px rgba(255,255,255,.08),
-        0 8px 28px rgba(216,50,58,.55),
+        0 0 0 3px #d8323a,
+        0 0 0 5px rgba(216,50,58,.18),
+        0 8px 24px rgba(216,50,58,.5),
         0 2px 10px rgba(0,0,0,.4); }
       50% { box-shadow:
-        0 0 0 2px rgba(255,255,255,.14),
-        0 8px 38px rgba(255,85,96,.78),
-        0 2px 14px rgba(0,0,0,.45); }
+        0 0 0 3px #ff5560,
+        0 0 0 7px rgba(255,85,96,.22),
+        0 8px 32px rgba(255,85,96,.7),
+        0 2px 12px rgba(0,0,0,.45); }
     }
+    @keyframes bigred-pulse-green {
+      0%, 100% { box-shadow:
+        0 0 0 3px #2fbf4a,
+        0 0 0 5px rgba(47,191,74,.20),
+        0 8px 24px rgba(47,191,74,.5),
+        0 2px 10px rgba(0,0,0,.4); }
+      50% { box-shadow:
+        0 0 0 3px #58e070,
+        0 0 0 8px rgba(88,224,112,.26),
+        0 8px 32px rgba(88,224,112,.72),
+        0 2px 12px rgba(0,0,0,.45); }
+    }
+
+    .bigred-tag {
+      position: fixed;
+      left: 96px;
+      bottom: 36px;
+      z-index: 99990;
+      pointer-events: none;
+      font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
+      font-weight: 900;
+      letter-spacing: 0.22em;
+      font-size: 10px;
+      color: #ff5560;
+      text-shadow: 0 0 12px rgba(255,85,96,.55), 0 0 2px rgba(0,0,0,.6);
+      opacity: 0;
+      transform: translateX(-6px);
+      transition: opacity .25s, transform .25s, color .25s, text-shadow .25s;
+    }
+    .bigred-launcher:hover + .bigred-tag,
+    .bigred-launcher.live + .bigred-tag { opacity: 1; transform: translateX(0); }
+    .bigred-launcher.live + .bigred-tag {
+      color: #58e070;
+      text-shadow: 0 0 12px rgba(88,224,112,.55), 0 0 2px rgba(0,0,0,.6);
+    }
+
     .bigred-panel {
       position: fixed;
       left: 22px;
-      bottom: 22px;
+      bottom: 96px;
       z-index: 99991;
-      width: min(420px, calc(100vw - 44px));
-      height: min(640px, calc(100vh - 44px));
+      width: min(440px, calc(100vw - 44px));
+      height: min(660px, calc(100vh - 130px));
       background: #1a0506;
       border: 1px solid #8b1419;
       border-radius: 18px;
@@ -113,34 +160,33 @@
       font-family: -apple-system, BlinkMacSystemFont, "SF Pro Text", "Inter", sans-serif;
       font-size: 11px;
       font-weight: 900;
-      letter-spacing: 0.18em;
+      letter-spacing: 0.22em;
       text-shadow: 0 0 12px rgba(255,85,96,.5);
     }
+    .bigred-bar .live-dot {
+      display: inline-block; width: 7px; height: 7px; border-radius: 50%;
+      background: #58e070; margin-right: 8px; vertical-align: middle;
+      box-shadow: 0 0 8px #58e070;
+      animation: bigred-livedot 1.4s ease-in-out infinite;
+    }
+    @keyframes bigred-livedot {
+      0%, 100% { opacity: 1; transform: scale(1); }
+      50% { opacity: 0.55; transform: scale(.85); }
+    }
     .bigred-bar .close {
-      background: transparent;
-      border: 0;
-      color: #ff5560;
-      cursor: pointer;
-      width: 26px;
-      height: 26px;
-      border-radius: 6px;
-      font-size: 18px;
-      line-height: 1;
-      display: flex;
-      align-items: center;
-      justify-content: center;
+      background: transparent; border: 0; color: #ff5560; cursor: pointer;
+      width: 26px; height: 26px; border-radius: 6px; font-size: 18px; line-height: 1;
+      display: flex; align-items: center; justify-content: center;
       transition: background .12s;
     }
     .bigred-bar .close:hover { background: rgba(255,85,96,.16); }
     .bigred-frame {
-      flex: 1;
-      width: 100%;
-      border: 0;
-      background: #1a0506;
+      flex: 1; width: 100%; border: 0; background: #1a0506;
     }
     @media (max-width: 640px) {
-      .bigred-launcher { left: 16px; bottom: 16px; width: 52px; height: 52px; font-size: 10px; }
-      .bigred-panel { left: 8px; right: 8px; bottom: 8px; width: auto; }
+      .bigred-launcher { left: 16px; bottom: 16px; width: 56px; height: 56px; }
+      .bigred-tag { left: 80px; bottom: 28px; font-size: 9px; }
+      .bigred-panel { left: 8px; right: 8px; bottom: 84px; width: auto; }
     }
   `;
 
@@ -152,25 +198,43 @@
   const launcher = document.createElement('button');
   launcher.type = 'button';
   launcher.className = 'bigred-launcher';
-  launcher.setAttribute('aria-label', 'Open Big Red chat');
-  launcher.innerHTML = '<span class="label">BIG<br>RED</span>';
+  launcher.setAttribute('aria-label', 'Open Big Red voice chat');
+  // Avatar image — falls back to Steve's stored avatar at /avatar/steve.jpg.
+  // Add a query bust so updated frames refresh on next page load.
+  const avatarImg = document.createElement('img');
+  avatarImg.alt = '';
+  avatarImg.src = host + '/avatar/steve.jpg?ts=' + Date.now();
+  avatarImg.onerror = () => {
+    // No avatar uploaded yet — fall back to a "BR" monogram glyph.
+    avatarImg.style.display = 'none';
+    const mono = document.createElement('div');
+    mono.style.cssText = 'width:100%;height:100%;display:flex;align-items:center;justify-content:center;font-family:-apple-system,sans-serif;font-weight:900;font-size:14px;letter-spacing:.04em;color:#fff;text-shadow:0 0 8px rgba(0,0,0,.6);background:#5a0d10;';
+    mono.textContent = 'BR';
+    launcher.appendChild(mono);
+  };
+  launcher.appendChild(avatarImg);
+
+  const tag = document.createElement('div');
+  tag.className = 'bigred-tag';
+  tag.textContent = 'TALK TO BIG RED';
 
   const panel = document.createElement('div');
   panel.className = 'bigred-panel';
   panel.setAttribute('role', 'dialog');
-  panel.setAttribute('aria-label', 'Big Red chat');
+  panel.setAttribute('aria-label', 'Big Red voice chat');
   panel.innerHTML = `
     <div class="bigred-bar">
-      <span>BIG RED</span>
+      <span><span class="live-dot"></span>BIG RED · VOICE</span>
       <button class="close" aria-label="Close">×</button>
     </div>
-    <iframe class="bigred-frame" title="Big Red chat"
-            allow="microphone; camera; clipboard-read; clipboard-write"
+    <iframe class="bigred-frame" title="Big Red voice chat"
+            allow="microphone; camera; clipboard-read; clipboard-write; autoplay"
             src="about:blank"
             loading="lazy"></iframe>
   `;
 
   document.body.appendChild(launcher);
+  document.body.appendChild(tag);
   document.body.appendChild(panel);
 
   const iframe = panel.querySelector('.bigred-frame');
@@ -178,16 +242,24 @@
 
   function open() {
     if (iframe.src === 'about:blank' || !iframe.src.startsWith(host)) {
-      iframe.src = host + '/?embed=1';
+      // ?embed=1&voice=1 — host UI can pick this up to default to voice mode.
+      iframe.src = host + '/?embed=1&voice=1';
     }
-    launcher.classList.add('open');
+    launcher.classList.add('live');
+    launcher.setAttribute('aria-label', 'Close Big Red (currently live)');
+    tag.textContent = 'LIVE — TAP TO HIDE';
     panel.classList.add('open');
   }
   function close() {
-    launcher.classList.remove('open');
+    launcher.classList.remove('live');
+    launcher.setAttribute('aria-label', 'Open Big Red voice chat');
+    tag.textContent = 'TALK TO BIG RED';
     panel.classList.remove('open');
   }
-  launcher.addEventListener('click', open);
+  function toggle() {
+    if (panel.classList.contains('open')) close(); else open();
+  }
+  launcher.addEventListener('click', toggle);
   closeBtn.addEventListener('click', close);
 
   // Esc closes the panel
@@ -195,6 +267,6 @@
     if (e.key === 'Escape' && panel.classList.contains('open')) close();
   });
 
-  // Expose a tiny API so host pages can toggle programmatically if they want
-  window.BigRed = { open, close, host };
+  // Public API
+  window.BigRed = { open, close, toggle, host };
 })();

← 83c3c52 feat(big-red): embeddable widget.js — floating launcher for  ·  back to Big Red  ·  widget: anchor bottom-right; slide launcher left of panel wh 74925bb →