← back to Big Red
feat: per-mode personas (retail / wholesale / admin) + app context
0f3f22bf6e0a684f64cb2b909b498cf1304117eb · 2026-05-12 00:09:26 -0700 · Steve Abrams
Widget reads data-mode + data-app from its <script> tag and propagates
both into the iframe URL params; the chat endpoint reads them from
body OR Referer query string and selects one of three personas:
- retail (default): warm customer-service, hides vendor/margin/wholesale
- wholesale: trade-savvy, surfaces vendor + MOQ + lead time + class A
- admin: direct operator voice for Steve, dense replies, code/SQL/shell
welcome, 240s Claude timeout (vs 60s for customer-facing modes)
Live tested: admin/lawyer-directory-builder → 'Need a clarification —
what's the integration point? Options: 1. chat widget on the site...'
(operator voice). Default retail → 'We've got beautiful silk...
smooth finishes with that lovely natural luster' (sales voice).
The DW SKU lookup (live Shopify price verify) still triggers on any
mode that mentions a SKU — same facts, different voice phrasing them.
Files touched
M public/widget.jsM server.js
Diff
commit 0f3f22bf6e0a684f64cb2b909b498cf1304117eb
Author: Steve Abrams <steve@designerwallcoverings.com>
Date: Tue May 12 00:09:26 2026 -0700
feat: per-mode personas (retail / wholesale / admin) + app context
Widget reads data-mode + data-app from its <script> tag and propagates
both into the iframe URL params; the chat endpoint reads them from
body OR Referer query string and selects one of three personas:
- retail (default): warm customer-service, hides vendor/margin/wholesale
- wholesale: trade-savvy, surfaces vendor + MOQ + lead time + class A
- admin: direct operator voice for Steve, dense replies, code/SQL/shell
welcome, 240s Claude timeout (vs 60s for customer-facing modes)
Live tested: admin/lawyer-directory-builder → 'Need a clarification —
what's the integration point? Options: 1. chat widget on the site...'
(operator voice). Default retail → 'We've got beautiful silk...
smooth finishes with that lovely natural luster' (sales voice).
The DW SKU lookup (live Shopify price verify) still triggers on any
mode that mentions a SKU — same facts, different voice phrasing them.
---
public/widget.js | 25 ++++++++++++++++++++---
server.js | 62 ++++++++++++++++++++++++++++++++++++++++++++++++--------
2 files changed, 75 insertions(+), 12 deletions(-)
diff --git a/public/widget.js b/public/widget.js
index 7984755..1890a06 100644
--- a/public/widget.js
+++ b/public/widget.js
@@ -18,6 +18,16 @@
(me && me.src ? new URL(me.src, location.href).origin : '') ||
'http://localhost:9935';
+ // Context — lets Big Red toggle between modes per site.
+ // data-mode="retail" → DW SKU lookup, live Shopify verify, customer voice
+ // data-mode="wholesale" → trade pricing, vendor margin, B2B voice
+ // data-mode="admin" → full Claude access, app-aware, tool-use enabled
+ // data-app="<slug>" → which internal app the bot is embedded in
+ // (admin-console, video-gallery, wallco-ai, etc.)
+ // Defaults: retail / current hostname.
+ const mode = (me && me.dataset && me.dataset.mode) || 'retail';
+ const appSlug = (me && me.dataset && me.dataset.app) || location.hostname.split('.')[0] || 'unknown';
+
const css = `
.bigred-launcher {
position: fixed;
@@ -245,8 +255,17 @@
function open() {
if (iframe.src === 'about:blank' || !iframe.src.startsWith(host)) {
- // ?embed=1&voice=1 — host UI can pick this up to default to voice mode.
- iframe.src = host + '/?embed=1&voice=1';
+ // Pass mode + app + page URL so Big Red knows which persona to load
+ // and what context the user is currently looking at.
+ const params = new URLSearchParams({
+ embed: '1',
+ voice: '1',
+ mode,
+ app: appSlug,
+ page: location.pathname + location.search,
+ title: document.title.slice(0, 120),
+ });
+ iframe.src = host + '/?' + params.toString();
}
launcher.classList.add('live');
launcher.setAttribute('aria-label', 'Close Big Red (currently live)');
@@ -271,5 +290,5 @@
});
// Public API
- window.BigRed = { open, close, toggle, host };
+ window.BigRed = { open, close, toggle, host, mode, app: appSlug };
})();
diff --git a/server.js b/server.js
index 7b3a8e4..d31db91 100644
--- a/server.js
+++ b/server.js
@@ -223,11 +223,48 @@ function runClaude(prompt, { imagePath = null, model = 'haiku', timeoutMs = 240_
}
// ---------- chat ----------
+// Per-mode persona — Big Red shows up differently on customer sites vs
+// internal admin tools. The widget passes mode + app via query params, the
+// host page can echo them into /api/chat body or we read them from the
+// Referer URL params. Default = retail (customer-facing).
+const MODE_PERSONAS = {
+ retail: {
+ label: 'retail',
+ voice: 'friendly, warm, customer-service',
+ bias: 'Steve is helping a designer or homeowner browse the wallcovering catalog. Suggest swatches, surface in-stock pieces, recommend pairings. Never expose vendor names, margins, or wholesale prices. If asked about pricing, give the retail price and offer a sample order.',
+ claude_timeout_ms: 60_000,
+ },
+ wholesale: {
+ label: 'wholesale',
+ voice: 'trade-savvy, concise, professional',
+ bias: 'The user is a trade-account designer or buyer. Surface vendor, MOQ, lead time, wholesale price tier, fire rating, contract Class A status when relevant. Same SKU lookup as retail but include vendor margin context.',
+ claude_timeout_ms: 60_000,
+ },
+ admin: {
+ label: 'admin',
+ voice: 'direct, no-fluff, Steve-as-operator',
+ bias: 'The user IS Steve, operating his fleet. Read app + page context from the system prompt. You have FULL Claude access — multi-paragraph, code, SQL, shell suggestions are all fair game. Give specific commands. Skip the "would you like…" pleasantries.',
+ claude_timeout_ms: 240_000,
+ },
+};
+
app.post('/api/chat', async (req, res) => {
- const { message, history } = req.body || {};
+ const { message, history, mode: bodyMode, app: bodyApp, page, title } = req.body || {};
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: 'message required' });
}
+ // Honor mode from body, else from Referer query string, else retail.
+ let mode = bodyMode || 'retail';
+ let appSlug = bodyApp || '';
+ try {
+ if (!bodyMode && req.headers.referer) {
+ const refUrl = new URL(req.headers.referer);
+ mode = refUrl.searchParams.get('mode') || mode;
+ appSlug = refUrl.searchParams.get('app') || appSlug;
+ }
+ } catch {}
+ if (!MODE_PERSONAS[mode]) mode = 'retail';
+ const persona = MODE_PERSONAS[mode];
try {
// DW intelligence — if the message mentions a SKU, enrich the prompt
// with live Shopify + dw_unified data BEFORE handing to Claude. The model
@@ -264,18 +301,25 @@ app.post('/api/chat', async (req, res) => {
.slice(-10)
.map((m) => `${m.role === 'user' ? 'User' : 'BigRed'}: ${m.content}`)
.join('\n');
+ const appLine = appSlug ? `Embedded in app: ${appSlug}` : '';
+ const pageLine = page ? `Page: ${page}${title ? ' — ' + title : ''}` : '';
const prompt = [
- 'You are "Big Red", a friendly desktop avatar assistant for Steve, the founder of Designer Wallcoverings.',
- 'You know his catalog: SKUs prefixed DWKK/DWSC/DWSW/DWWD/DWQW/DWJS/etc. map to live Shopify products. When a SKU is mentioned, you may have been given live lookup data below — use it verbatim, never fabricate prices.',
- 'Reply conversationally in 1-3 short sentences unless he asks for detail.',
- 'Plain prose only — no markdown, no lists, no code fences. The reply will be spoken aloud.',
+ `You are "Big Red", a chat companion for Steve (founder, Designer Wallcoverings).`,
+ `MODE: ${persona.label} — voice ${persona.voice}.`,
+ `Persona bias: ${persona.bias}`,
+ appLine,
+ pageLine,
+ 'You know the DW catalog — SKUs prefixed DWKK/DWSC/DWSW/DWWD/DWQW/DWJS/etc. map to live Shopify products. When a SKU is mentioned, you may have been given live lookup data below — use it verbatim, never fabricate prices.',
+ mode === 'admin'
+ ? 'Reply directly and densely — Steve is operating, not chatting. SQL snippets, shell commands, and code are welcome. Skip the conversational throat-clearing.'
+ : 'Reply conversationally in 1-3 short sentences unless asked for detail. Plain prose only — no markdown, no lists, no code fences. The reply will be spoken aloud.',
ctx ? `\nConversation so far:\n${ctx}` : '',
dwContext,
- `\nSteve: ${message}`,
+ `\nUser: ${message}`,
'\nBigRed:'
- ].join('');
- const reply = await runClaude(prompt, { timeoutMs: 240_000 });
- res.json({ reply, dw: dwLookup });
+ ].filter(Boolean).join('\n');
+ const reply = await runClaude(prompt, { timeoutMs: persona.claude_timeout_ms });
+ res.json({ reply, dw: dwLookup, mode, app: appSlug });
} catch (e) {
res.status(500).json({ error: String(e.message || e) });
}
← 62eb53c feat(big-red): DW intelligence — SKU lookup with live Shopif
·
back to Big Red
·
feat(big-red/tick0): chat-only /embed.html (panel iframe rep bd46b8f →