Files
hermes-mcp/product/chat-widget.js
Garfield be1a14f783 feat(chat): upgrade to full agentic demo bot (Option B)
Chat widget now runs a live tool-use loop via Claude Haiku. Exposes
slack, discord, and telegram demo tools — bot can actually send messages
and read channels to prove the platform works in real time. Widget shows
a purple pill with tool names when the agent calls a live platform.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 10:49:18 -04:00

192 lines
6.9 KiB
JavaScript

(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-tools-used {
font-size: 11px; color: #6c47ff; background: #f1f0fe;
border-radius: 6px; padding: 4px 8px; align-self: flex-start;
display: flex; align-items: center; gap: 5px;
}
#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">&times;</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">&#9658;</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 });
if (data.toolsUsed && data.toolsUsed.length > 0) {
const pill = document.createElement('div');
pill.className = 'smcp-tools-used';
pill.textContent = '⚡ Live: ' + data.toolsUsed.join(', ');
messages.appendChild(pill);
}
} 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();
}
})();