← 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 →