feat: Slack platform + Claude-powered chat support widget
- Add Slack as customer-facing messaging platform (client, 4 MCP tools, dashboard card) - Add /api/chat endpoint powered by Claude Haiku with SquareMCP system prompt - Add embeddable chat-widget.js injected into all 3 sites (docs, app, www) - Add ANTHROPIC_API_KEY, serve product/ as static files - Update Platform type to include slack Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
180
product/chat-widget.js
Normal file
180
product/chat-widget.js
Normal file
@@ -0,0 +1,180 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const API = 'https://hermes.squaremcp.com/api/chat';
|
||||
const BRAND = '#6c47ff';
|
||||
const BRAND_DARK = '#5535e0';
|
||||
|
||||
const css = `
|
||||
#smcp-btn {
|
||||
position: fixed; bottom: 24px; right: 24px; z-index: 9998;
|
||||
width: 56px; height: 56px; border-radius: 50%;
|
||||
background: ${BRAND}; border: none; cursor: pointer;
|
||||
box-shadow: 0 4px 16px rgba(0,0,0,0.18);
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
transition: background 0.2s;
|
||||
}
|
||||
#smcp-btn:hover { background: ${BRAND_DARK}; }
|
||||
#smcp-btn svg { width: 26px; height: 26px; }
|
||||
#smcp-window {
|
||||
position: fixed; bottom: 92px; right: 24px; z-index: 9999;
|
||||
width: 360px; height: 520px; border-radius: 16px;
|
||||
background: #fff; box-shadow: 0 8px 40px rgba(0,0,0,0.18);
|
||||
display: flex; flex-direction: column; overflow: hidden;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||
font-size: 14px;
|
||||
}
|
||||
#smcp-window.hidden { display: none; }
|
||||
#smcp-header {
|
||||
background: ${BRAND}; color: #fff;
|
||||
padding: 14px 16px; display: flex; align-items: center; gap: 10px;
|
||||
font-weight: 600; font-size: 15px; flex-shrink: 0;
|
||||
}
|
||||
#smcp-header span { flex: 1; }
|
||||
#smcp-close {
|
||||
background: none; border: none; color: #fff; cursor: pointer;
|
||||
font-size: 20px; line-height: 1; padding: 0;
|
||||
}
|
||||
#smcp-messages {
|
||||
flex: 1; overflow-y: auto; padding: 16px;
|
||||
display: flex; flex-direction: column; gap: 10px;
|
||||
}
|
||||
.smcp-msg {
|
||||
max-width: 80%; padding: 9px 13px; border-radius: 12px;
|
||||
line-height: 1.45; word-break: break-word;
|
||||
}
|
||||
.smcp-msg.bot {
|
||||
background: #f1f0fe; color: #1a1a2e; border-bottom-left-radius: 4px; align-self: flex-start;
|
||||
}
|
||||
.smcp-msg.user {
|
||||
background: ${BRAND}; color: #fff; border-bottom-right-radius: 4px; align-self: flex-end;
|
||||
}
|
||||
.smcp-msg.typing { color: #888; font-style: italic; }
|
||||
#smcp-input-row {
|
||||
padding: 10px 12px; border-top: 1px solid #eee;
|
||||
display: flex; gap: 8px; flex-shrink: 0;
|
||||
}
|
||||
#smcp-input {
|
||||
flex: 1; border: 1px solid #ddd; border-radius: 8px;
|
||||
padding: 8px 12px; font-size: 14px; outline: none;
|
||||
font-family: inherit; resize: none; line-height: 1.4;
|
||||
}
|
||||
#smcp-input:focus { border-color: ${BRAND}; }
|
||||
#smcp-send {
|
||||
background: ${BRAND}; color: #fff; border: none;
|
||||
border-radius: 8px; padding: 0 14px; cursor: pointer;
|
||||
font-size: 18px; transition: background 0.2s;
|
||||
}
|
||||
#smcp-send:hover { background: ${BRAND_DARK}; }
|
||||
#smcp-send:disabled { background: #ccc; cursor: default; }
|
||||
@media (max-width: 420px) {
|
||||
#smcp-window { width: calc(100vw - 24px); right: 12px; bottom: 80px; }
|
||||
}
|
||||
`;
|
||||
|
||||
const WELCOME = "Hi! I'm the SquareMCP assistant. Ask me anything about connecting your AI agent to social platforms.";
|
||||
|
||||
function inject() {
|
||||
const style = document.createElement('style');
|
||||
style.textContent = css;
|
||||
document.head.appendChild(style);
|
||||
|
||||
document.body.insertAdjacentHTML('beforeend', `
|
||||
<button id="smcp-btn" aria-label="Chat with SquareMCP">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</button>
|
||||
<div id="smcp-window" class="hidden">
|
||||
<div id="smcp-header">
|
||||
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
<span>SquareMCP Support</span>
|
||||
<button id="smcp-close">×</button>
|
||||
</div>
|
||||
<div id="smcp-messages"></div>
|
||||
<div id="smcp-input-row">
|
||||
<textarea id="smcp-input" rows="1" placeholder="Ask a question…"></textarea>
|
||||
<button id="smcp-send">►</button>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
|
||||
const win = document.getElementById('smcp-window');
|
||||
const btn = document.getElementById('smcp-btn');
|
||||
const closeBtn = document.getElementById('smcp-close');
|
||||
const messages = document.getElementById('smcp-messages');
|
||||
const input = document.getElementById('smcp-input');
|
||||
const send = document.getElementById('smcp-send');
|
||||
|
||||
const history = [];
|
||||
let busy = false;
|
||||
|
||||
function addMsg(text, role, typing) {
|
||||
const div = document.createElement('div');
|
||||
div.className = 'smcp-msg ' + role + (typing ? ' typing' : '');
|
||||
div.textContent = text;
|
||||
messages.appendChild(div);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
return div;
|
||||
}
|
||||
|
||||
addMsg(WELCOME, 'bot');
|
||||
|
||||
btn.addEventListener('click', () => {
|
||||
win.classList.toggle('hidden');
|
||||
if (!win.classList.contains('hidden')) input.focus();
|
||||
});
|
||||
closeBtn.addEventListener('click', () => win.classList.add('hidden'));
|
||||
|
||||
async function submit() {
|
||||
const text = input.value.trim();
|
||||
if (!text || busy) return;
|
||||
busy = true;
|
||||
send.disabled = true;
|
||||
input.value = '';
|
||||
input.style.height = '';
|
||||
|
||||
addMsg(text, 'user');
|
||||
history.push({ role: 'user', content: text });
|
||||
|
||||
const indicator = addMsg('Typing…', 'bot', true);
|
||||
|
||||
try {
|
||||
const res = await fetch(API, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ messages: history }),
|
||||
});
|
||||
const data = await res.json();
|
||||
const reply = data.reply || 'Sorry, something went wrong. Try again.';
|
||||
indicator.textContent = reply;
|
||||
indicator.classList.remove('typing');
|
||||
history.push({ role: 'assistant', content: reply });
|
||||
} catch {
|
||||
indicator.textContent = 'Network error. Please try again.';
|
||||
indicator.classList.remove('typing');
|
||||
}
|
||||
|
||||
busy = false;
|
||||
send.disabled = false;
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
}
|
||||
|
||||
send.addEventListener('click', submit);
|
||||
input.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); submit(); }
|
||||
});
|
||||
input.addEventListener('input', () => {
|
||||
input.style.height = '';
|
||||
input.style.height = Math.min(input.scrollHeight, 96) + 'px';
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', inject);
|
||||
} else {
|
||||
inject();
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user