[object Object]

← back to Contact Mailer Ideas

viewer: dedicated renderers for SWOT (5 colored tiles + competitor chips) and marketing (channel/hook/repeat + MCP chips); SWOT and marketing pinned to top of artifacts list

008d6db868fab47830811fc7011d9f241ffea03d · 2026-05-11 16:19:34 -0700 · Steve Abrams

Files touched

Diff

commit 008d6db868fab47830811fc7011d9f241ffea03d
Author: Steve Abrams <steve@designerwallcoverings.com>
Date:   Mon May 11 16:19:34 2026 -0700

    viewer: dedicated renderers for SWOT (5 colored tiles + competitor chips) and marketing (channel/hook/repeat + MCP chips); SWOT and marketing pinned to top of artifacts list
---
 viewer/server.js | 64 +++++++++++++++++++++++++++++++++++++++++++++++++-------
 1 file changed, 57 insertions(+), 7 deletions(-)

diff --git a/viewer/server.js b/viewer/server.js
index 8c0df66..2856a26 100644
--- a/viewer/server.js
+++ b/viewer/server.js
@@ -585,15 +585,65 @@ function openDialog(idx){
   bindCardEvents();
 }
 function renderArtifacts(arts){
-  const labels = { pitch:'🎤 Investor pitch', coldlines:'📧 Cold-email openers', competitors:'🥊 Competitor matrix', spike:'🧱 MVP architecture', pricing:'💰 Pricing' };
+  const labels = {
+    swot:'⚖️ SWOT + Competitor Landscape',
+    marketing:'📣 Go-to-market + MCPs Needed',
+    pitch:'🎤 Investor pitch',
+    coldlines:'📧 Cold-email openers',
+    competitors:'🥊 Competitor matrix',
+    spike:'🧱 MVP architecture',
+    pricing:'💰 Pricing'
+  };
+  // Reorder: swot + marketing first (since they're synthesis), then everything else
+  const priority = ['swot','marketing','pitch','spike','pricing','competitors','coldlines'];
+  arts = [...arts].sort((a,b) => (priority.indexOf(a.artifact_type) - priority.indexOf(b.artifact_type)));
   return '<div style="margin-top:20px;border-top:1px solid #1f1f27;padding-top:18px">' +
     '<h3 style="font-size:11px;letter-spacing:.15em;color:#7a7a82;margin-bottom:14px;text-transform:uppercase">Opportunity assets</h3>' +
-    arts.map(a => \`
-      <details style="background:#0d0d12;border:1px solid #1c1c22;border-radius:8px;padding:12px 14px;margin-bottom:10px" \${arts.indexOf(a) === 0 ? 'open' : ''}>
-        <summary style="cursor:pointer;font-size:13px;font-weight:600;color:#cfcfd5;outline:none">\${labels[a.artifact_type] || a.artifact_type} <span style="color:#5a5a62;font-weight:400;font-size:11px;margin-left:8px">tick \${a.tick}</span></summary>
-        <pre style="white-space:pre-wrap;font-family:'SF Mono',Menlo,monospace;font-size:12.5px;line-height:1.55;color:#bcbcc4;margin-top:12px">\${escapeHtml(a.artifact)}</pre>
-      </details>
-    \`).join('') + '</div>';
+    arts.map((a, ix) => {
+      const body = renderArtifactBody(a);
+      return \`
+        <details style="background:#0d0d12;border:1px solid #1c1c22;border-radius:8px;padding:12px 14px;margin-bottom:10px" \${ix === 0 ? 'open' : ''}>
+          <summary style="cursor:pointer;font-size:13px;font-weight:600;color:#cfcfd5;outline:none">\${labels[a.artifact_type] || a.artifact_type} <span style="color:#5a5a62;font-weight:400;font-size:11px;margin-left:8px">tick \${a.tick}</span></summary>
+          <div style="margin-top:12px">\${body}</div>
+        </details>
+      \`;
+    }).join('') + '</div>';
+}
+function renderArtifactBody(a){
+  if (a.artifact_type === 'swot' && a.swot) {
+    const s = a.swot;
+    const tile = (label, color, text) => \`<div style="background:\${color}1a;border-left:3px solid \${color};padding:10px 12px;border-radius:6px;margin-bottom:8px"><div style="font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:\${color};margin-bottom:4px">\${label}</div><div style="font-size:13px;color:#d4d4d8;line-height:1.5">\${escapeHtml(text)}</div></div>\`;
+    let html = '';
+    html += tile('Strengths', '#22c55e', s.strengths || '—');
+    html += tile('Weaknesses', '#ef4444', s.weaknesses || '—');
+    html += tile('Opportunities', '#0EA5E9', s.opportunities || '—');
+    html += tile('Threats', '#f59e0b', s.threats || '—');
+    if (s.competitors && s.competitors.length){
+      html += '<div style="margin-top:6px"><div style="font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:#a78bfa;margin-bottom:8px">Competitor Landscape</div>';
+      html += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
+      html += s.competitors.map(c => \`<span style="background:#1c1c24;border:1px solid #2a2a31;color:#cfcfd5;padding:5px 10px;border-radius:6px;font-size:12px">\${escapeHtml(c)}</span>\`).join('');
+      html += '</div></div>';
+    }
+    return html;
+  }
+  if (a.artifact_type === 'marketing' && a.marketing) {
+    const m = a.marketing;
+    const mcps = a.mcps_needed || [];
+    const row = (label, color, text) => \`<div style="margin-bottom:10px"><div style="font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:\${color};margin-bottom:4px">\${label}</div><div style="font-size:13px;color:#d4d4d8;line-height:1.5">\${escapeHtml(text)}</div></div>\`;
+    let html = '';
+    html += row('Channel', '#0EA5E9', m.channel || '—');
+    html += row('Hook', '#a78bfa', m.hook || '—');
+    html += row('Repeat / network effect', '#22c55e', m.repeat || '—');
+    if (mcps.length){
+      html += '<div style="margin-top:14px;padding-top:12px;border-top:1px dashed #2a2a31"><div style="font-size:10.5px;font-weight:700;letter-spacing:.12em;text-transform:uppercase;color:#fb923c;margin-bottom:8px">MCPs / Skills / APIs needed</div>';
+      html += '<div style="display:flex;flex-wrap:wrap;gap:6px">';
+      html += mcps.map(c => \`<span style="background:#2a1a0a;border:1px solid #7c2d12;color:#fed7aa;padding:5px 10px;border-radius:6px;font-size:11.5px;font-family:'SF Mono',Menlo,monospace">\${escapeHtml(c)}</span>\`).join('');
+      html += '</div></div>';
+    }
+    return html;
+  }
+  // Default: pre-formatted text
+  return \`<pre style="white-space:pre-wrap;font-family:'SF Mono',Menlo,monospace;font-size:12.5px;line-height:1.55;color:#bcbcc4;margin:0">\${escapeHtml(a.artifact)}</pre>\`;
 }
 window.openIdeaDialog = openDialog;
 function renderRowActions(i){

← a514e3f viewer: deep-link by batch slug not date (multiple batches p  ·  back to Contact Mailer Ideas  ·  VenturaEats (PULSE #21, 33/40): Ventura-corridor restaurant b381acf →