Chapter 15 / 15 · App · AI surfaces & celebration

App AI. Sage in the room.

Where Sage shows up — in the right rail as a chat, in the compose drawer as suggestions, in detail pages as proactive cards. Plus the KB-suggestion pattern for support agents and a confetti win moment for closed deals (the only time we throw confetti).

15.1 Sage AI drawer

A right-anchored AI assistant. 360px wide (resizable 320–520 via drag-handle). Header with the “Sage” Fraunces wordmark + context chip (“on this deal” / “everywhere”) + close (⌘.). Message stream below: assistant + user turns, plus tool-call cards (collapsed by default) and proposal cards (highlighted with --accent-soft + Accept / Edit / Dismiss). Composer at the bottom with starter-prompt chips when empty. Streaming dots while thinking. Reduced motion: no pulsing dots, replaces with “thinking…” text.

Conversation in flight

.sage-drawer

Showing user message, assistant reply, expanded tool-call card, proposal card with action buttons, then streaming dots while a follow-up generates. In production the drawer is fixed to the right edge of the viewport at 100vh; the demo uses a constrained box to fit on the chapter page.

Sage
On this deal · BlueRock
You
What's the next best action for BlueRock?
Sage
BlueRock's renewal date is in 23 days and they haven't opened the proposal you sent on Apr 18. From the playbook, this is when re-engagement matters most.
tool get_recent_activity(deal_id=42)
Returned 8 events. Last touchpoint: email "Renewal proposal" sent Apr 18, no opens. Previous engagement: Q1 QBR Mar 12 (very positive).
Sage suggests
Send a follow-up email to Alicia today, with subject "Quick check-in on the renewal" — friendly, short, with a calendar link for a 15-min sync.
<aside class="sage-drawer">
  <header class="sage-head">
    <div class="sage-row1">
      <span class="sage-wordmark">
        <svg class="sage-spark">...</svg>Sage
      </span>
      <button class="sage-close">×</button>
    </div>
    <span class="sage-context">On this deal · BlueRock</span>
  </header>

  <div class="sage-stream">
    <div class="sage-msg is-user">...</div>
    <div class="sage-msg is-assistant">...</div>

    <!-- Tool call (collapsed by default) -->
    <details class="sage-tool-call">
      <summary>tool · get_recent_activity(deal_id=42)</summary>
      <div class="sage-tc-body">...result...</div>
    </details>

    <!-- Proposal card (highlighted; Accept/Edit/Dismiss) -->
    <div class="sage-proposal">
      <div class="sage-proposal-head">✦ Sage suggests</div>
      <div class="sage-proposal-body">...</div>
      <div class="sage-proposal-actions">
        <button class="sage-prop-accept">Accept & draft</button>
        <button class="sage-prop-edit">Edit prompt</button>
        <button class="sage-prop-dismiss">Dismiss</button>
      </div>
    </div>

    <!-- Streaming dots -->
    <div class="sage-typing">
      <span class="sage-typing-dot"></span>
      <span class="sage-typing-dot"></span>
      <span class="sage-typing-dot"></span>
    </div>
  </div>

  <footer class="sage-composer">
    <div class="sage-prompts">
      <button class="sage-prompt-chip">Summarise this deal</button>
      ...
    </div>
    <div class="sage-input-row">
      <textarea class="sage-input" placeholder="Ask anything"></textarea>
      <button class="sage-send">Send</button>
    </div>
  </footer>
</aside>
.sage-drawer { width: 360px; height: 100vh;
  position: fixed; right: 0; top: 0;
  background: var(--bg-paper); border-left: 1px solid var(--hair);
  display: grid; grid-template-rows: auto 1fr auto; }

.sage-head { background: var(--gradient-glow-soft);
  padding: var(--s-4) var(--s-5); border-bottom: 1px solid var(--hair); }
.sage-wordmark { font-family: var(--f-serif); font-style: italic;
  font-size: 22px; font-variation-settings: "SOFT" 80; }

.sage-typing-dot { animation: sage-pulse 1.2s ease infinite; }
.sage-typing-dot:nth-child(2) { animation-delay: 0.2s; }
.sage-typing-dot:nth-child(3) { animation-delay: 0.4s; }
@media (prefers-reduced-motion: reduce) {
  .sage-typing-dot { display: none; }
  .sage-typing::after { content: "thinking…"; }
}

/* Resizable: drag the left edge between 320px and 520px */
.sage-drawer { resize: horizontal; min-width: 320px; max-width: 520px; }

15.2 Compose drawer

The single email composer used everywhere we send mail. Right-side overlay 540px. Header: From (rep selector for admin), To/CC/BCC autocomplete, schedule pill, tracking toggle, expand-to-fullscreen icon. Subject input. Rich-text body with slash-commands palette, ;intro shortcuts, and {{ }} variable autocomplete. AI assist bar above the body: Make warmer · Shorter · More direct · Add ROI line · Custom prompt. Footer: Send · Schedule · Save snippet. Draft autosaves silently — pulse → check.

Composer mid-write

.compose

Showing one recipient chip, schedule + tracking pills, AI assist bar, and a body with a variable token ({{first_name}}) that highlights as it's typed. The body is contenteditable in production; here it's static HTML for the demo.

New email

From jay@magicblocks.ai
To Alicia Chen
⏰ Schedule ✓ Tracking on · Slash / for commands · ;intro for snippets · {{ }} for variables
Sage assist

Hi {{first_name}},

Wanted to drop a friendly note about the renewal proposal we sent on Apr 18. Happy to walk through any questions on a quick 15-min sync this week if helpful — here's my calendar.

Either way, looking forward to closing this out together. The team at MagicBlocks is genuinely excited about the next year with you.

Best,
Jay

Draft saved · 2s ago
<aside class="compose">
  <header class="compose-head">
    <h4>New email</h4>
    <div class="compose-head-actions">
      <button class="compose-icon-btn">⛶</button>  <!-- expand -->
      <button class="compose-icon-btn">×</button>  <!-- close -->
    </div>
  </header>

  <div class="compose-headers">
    <div class="compose-header-row">...From...</div>
    <div class="compose-header-row">...To with chips...</div>
    <div class="compose-header-row">...Cc / Bcc reveal...</div>
  </div>

  <div class="compose-meta-row">
    <span class="compose-meta-pill">⏰ Schedule</span>
    <span class="compose-meta-pill is-on">✓ Tracking on</span>
  </div>

  <input class="compose-subject" placeholder="Subject…">

  <!-- AI assist bar -->
  <div class="compose-ai-bar">
    <span class="compose-ai-eyebrow">✦ Sage assist</span>
    <button class="compose-ai-chip">Make warmer</button>
    <button class="compose-ai-chip">Shorter</button>
    ...
  </div>

  <div class="compose-body" contenteditable="true">
    <p>Hi <code class="var-tag">{{first_name}}</code>,</p>
    ...
  </div>

  <footer class="compose-foot">
    <div class="compose-autosave">
      <span class="compose-autosave-dot"></span>
      Draft saved · 2s ago
    </div>
    <div class="compose-foot-actions">
      <button>Save snippet</button>
      <button>Schedule</button>
      <button class="compose-send-btn">Send</button>
    </div>
  </footer>
</aside>
.compose { width: 540px;
  background: var(--bg-paper); border: 1px solid var(--hair);
  border-radius: var(--r-lg); box-shadow: var(--sh-3);
  display: grid; grid-template-rows: auto auto auto auto auto 1fr auto; }

.compose-ai-bar { background: var(--ai-glow-bg);
  padding: var(--s-3) var(--s-4); }
.compose-ai-chip { background: var(--bg-paper);
  border: 1px solid color-mix(in oklab, var(--accent) 20%, transparent);
  border-radius: var(--r-pill); padding: 5px 10px;
  font: 500 11px var(--f-body); color: var(--fg); }

.compose-body code.var-tag {
  display: inline-block; padding: 1px 6px;
  background: var(--accent-soft); color: var(--accent-text);
  border-radius: var(--r-xs);
  font: 500 12.5px var(--f-mono); }

15.3 AI suggestion card

Sage's proactive surface inside detail pages. Card with a subtle gradient (using --ai-glow-bg), spark icon + “Sage suggests” eyebrow, one-paragraph suggestion, primary CTA + secondary “Edit” + dismiss. Three states: generating (shimmer), ready (default), dismissed (collapsed to a chip “Sage has 1 idea”).

Three states

.ai-card

In the right rail of a detail page (13.1) it sits naturally above other context cards. The dismiss action shouldn't lose the suggestion — it collapses to a small chip the user can re-open.

Sage suggests

Send the renewal proposal today. BlueRock's renewal date is in 23 days. The pattern from past closes: send proposal > 14 days out for a 70% acceptance rate.

Sage suggests

Loading…

<!-- Ready state -->
<div class="ai-card">
  <div class="ai-card-head">
    <span class="ai-card-eyebrow">✦ Sage suggests</span>
    <button class="ai-card-dismiss">×</button>
  </div>
  <p class="ai-card-body">Send the renewal proposal <em>today</em>. BlueRock's renewal date is in 23 days...</p>
  <div class="ai-card-actions">
    <button class="ai-card-cta">Draft email</button>
    <button class="ai-card-edit">Edit prompt</button>
  </div>
</div>

<!-- Generating state -->
<div class="ai-card is-generating">...</div>

<!-- Dismissed → collapsed chip -->
<button class="ai-card-chip">✦ Sage has 1 idea</button>
.ai-card { background: var(--bg-paper);
  border: 1px solid color-mix(in oklab, var(--accent) 25%, var(--hair));
  border-radius: var(--r-lg); padding: var(--s-4);
  position: relative; overflow: hidden; }
.ai-card::before { content: ""; position: absolute; inset: 0;
  background: var(--ai-glow-bg);     /* dark mode: 1.5× opacity */
  pointer-events: none; }
.ai-card > * { position: relative; z-index: 1; }

.ai-card.is-generating .ai-card-body { color: transparent; }
.ai-card.is-generating .ai-card-body::after {
  content: "Sage is thinking…";
  font: 400 13px var(--f-mono); color: var(--fg-dim); font-style: italic; }

15.4 KB suggestion card

Sage's “assist” surface for support agents. Shows a relevant KB article with title, snippet (matching highlight), helpful/not-helpful counts, and three actions: Insert into reply, Open article, Mark not helpful. The matched search term is wrapped in a <mark> with --accent-soft.

Three suggestions for the current ticket

.kb-card-suggest

Stacks vertically in the right rail. The agent can insert any one with one click; helpful counts feed back into Sage's ranking model.

Configuring SAML SSO with Okta

KB · 4 min read · last updated Apr 12

If your customer is using Okta, navigate to Settings → Authentication and choose SAML SSO. The metadata URL we provide goes into Okta's app settings; we'll handle the rest…

92% helpful · 47 reads

Troubleshooting SAML signature mismatch

KB · 2 min read · last updated Mar 28

Most SAML errors come from a clock skew between the IdP and our service. Confirm both sides are syncing to NTP, then re-test…

78% helpful · 22 reads
<div class="kb-card-suggest">
  <div class="kb-card-suggest-head">
    <span class="kb-card-suggest-icon">...</span>
    <div>
      <h4 class="kb-card-suggest-title">Configuring SAML SSO</h4>
      <div class="kb-card-suggest-meta">KB · 4 min · last updated Apr 12</div>
    </div>
  </div>
  <p class="kb-card-suggest-snippet">
    If your customer is using Okta… choose <mark>SAML SSO</mark> …
  </p>
  <div class="kb-card-suggest-foot">
    <span class="kb-card-suggest-helpful">
      <strong>92%</strong> helpful · 47 reads
    </span>
    <div class="kb-card-suggest-actions">
      <button class="kb-action-primary">Insert into reply</button>
      <button>Open</button>
    </div>
  </div>
</div>
.kb-card-suggest { background: var(--bg-paper);
  border: 1px solid var(--hair); border-radius: var(--r-md);
  padding: var(--s-4); }

.kb-card-suggest-snippet mark {
  background: var(--accent-soft); color: var(--fg);
  padding: 1px 3px; border-radius: 2px; }

.kb-action-primary { background: var(--accent); color: var(--paper);
  border-color: var(--accent); }

15.5 Confetti win moment

Reserved for closed-won deals (and only those). Full-screen transparent overlay; ~30 small spans drift down from centre over 1.4s with brand-palette particle colours (pink, blue, warm, ink). Paired with a one-line toast (“🎉 Won — $36k ARR. Nice.”). Under reduced motion, particles are suppressed; the toast still fires.

Click to fire

.confetti-stage · MB.confetti.fire()

In production this is a tiny JS helper (per spec § 27): 30 spans appended to body, CSS keyframes do the drift, helper removes them after 1.6s. The demo scopes it inside the stage so it doesn't take over the chapter page.

Won — $36k ARR. Nice.
<!-- Markup is just the toast; particles are injected by JS -->
<div class="confetti-toast">
  🎉 Won — <span class="em">$36k</span> ARR. Nice.
</div>

<!-- Helper (drop into _shared.js) -->
<script>
  window.MB = window.MB || {};
  MB.confetti = {
    fire(opts = {}) {
      const reduce = matchMedia('(prefers-reduced-motion: reduce)').matches;
      if (reduce) return;  // toast still fires; particles don't
      const colors = ['var(--accent)', 'var(--info)', 'var(--warning)', 'var(--ink)'];
      const host = opts.host || document.body;
      for (let i = 0; i < 30; i++) {
        const el = document.createElement('span');
        el.className = 'confetti-particle';
        el.style.background = colors[i % colors.length];
        // Random end position (full-screen spread)
        el.style.setProperty('--cx', `${(Math.random() * 600 - 300)}px`);
        el.style.setProperty('--cr', `${Math.random() * 720 - 360}deg`);
        el.style.animationDelay = `${Math.random() * 0.2}s`;
        host.appendChild(el);
        setTimeout(() => el.remove(), 1600);
      }
    }
  };
</script>
.confetti-particle {
  position: absolute; top: 50%; left: 50%;
  width: 8px; height: 12px;
  background: var(--accent);
  opacity: 0; pointer-events: none;
}
.confetti-stage.is-firing .confetti-particle {
  animation: confetti-drift 1.4s var(--ease) forwards;
}
@keyframes confetti-drift {
  0%   { opacity: 0; transform: translate(-50%, -50%) rotate(0deg); }
  10%  { opacity: 1; }
  100% { opacity: 0; transform: translate(var(--cx, 0), 250px) rotate(var(--cr, 360deg)); }
}
@media (prefers-reduced-motion: reduce) {
  .confetti-stage.is-firing .confetti-particle { animation: none; opacity: 0; }
}