Dave's trained on my Untangle Your Brand methodology. Ask him something specific and he'll answer it straight, with links through to the articles where there's more to read. Ask him about brands, bands, or anything you like. He's here to help bring clarity to your brand and share insight you might not have spotted. Nothing you share with him is stored or carried beyond the session.
super-embed:
<style>
#dave-chat-widget { width: 60%; }
@media (max-width: 768px) {
#dave-chat-widget { width: 92%; }
}
</style>
<div id="dave-chat-widget" style="margin:2rem auto;font-family:inherit;">
<div id="dave-thread" style="display:flex;flex-direction:column;gap:1rem;margin-bottom:1rem;"></div>
<div style="display:flex;flex-direction:column;gap:0.5rem;">
<textarea id="dave-question" rows="1" placeholder="Ask Dave a question..." style="width:100%;padding:0.75rem 1rem;border:1px solid #ccc;border-radius:6px;font-size:1rem;box-sizing:border-box;font-family:inherit;resize:none;overflow-y:hidden;max-height:200px;line-height:1.5;"></textarea>
<button id="dave-ask-btn" style="align-self:flex-start;padding:0.75rem 1.25rem;background:#000;color:#fff;border:none;border-radius:6px;font-size:1rem;cursor:pointer;width:130px;box-sizing:border-box;text-align:center;white-space:nowrap;">Ask</button>
</div>
<p style="margin:0.75rem 0 0;font-size:0.85rem !important;color:#888;line-height:1.5;-webkit-text-size-adjust:100%;text-size-adjust:100%;">Dave is AI. He can be inconsistent or mistaken, so treat this as reflective, not authoritative.</p>
</div>
<script>
(function() {
const DAVE_ENDPOINT = "<https://dave-worker.pin-creative.workers.dev/chat>";
const OG_IMAGE_ENDPOINT = "<https://dave-worker.pin-creative.workers.dev/og-image>";
const input = document.getElementById('dave-question');
const btn = document.getElementById('dave-ask-btn');
const threadEl = document.getElementById('dave-thread');
// Thread lives in memory only - resets on page refresh, sent in full with
// every request since the Worker keeps no server-side conversation state.
const messages = [];
function appendUserMessage(text) {
const row = document.createElement('div');
row.style.cssText = 'font-weight:600;line-height:1.6;white-space:pre-wrap;';
row.textContent = text;
threadEl.appendChild(row);
}
function appendAssistantMessage(text) {
const wrap = document.createElement('div');
const answerEl = document.createElement('div');
answerEl.style.cssText = 'line-height:1.6;white-space:pre-wrap;';
answerEl.textContent = text;
const sourcesEl = document.createElement('div');
sourcesEl.style.cssText = 'margin-top:0.35rem;font-size:0.85rem;line-height:2.6;color:#666;-webkit-text-size-adjust:100%;text-size-adjust:100%;';
wrap.appendChild(answerEl);
wrap.appendChild(sourcesEl);
threadEl.appendChild(wrap);
return { wrap: wrap, answerEl: answerEl, sourcesEl: sourcesEl };
}
// Scrolls so the answer sits just below the site's sticky nav, rather than
// under it. Computed manually (not el.scrollIntoView) because
// scrollIntoView has no way to know about a sticky/fixed header eating
// into the top of the viewport - it aligns to the raw viewport edge, so
// the nav (.super-navbar, ~84px) would cover the first chunk of the
// answer even when "successful". Called exactly once, after the final
// content (answer text + sources) is in the DOM.
//
// behavior is 'instant', not 'smooth' - live-tested on the real page and
// confirmed window.scrollTo({behavior:'smooth'}) is a silent no-op here
// (scrollY never moves, isolated from the widget entirely and reproduced
// repeatedly), while 'instant' works immediately every time. Something on
// the page (Next.js's own scroll handling is the likely candidate) swallows
// smooth-scroll animation frames; instant scrolling sidesteps that
// entirely rather than fighting it.
function scrollToAnswer(el) {
const nav = document.querySelector('.super-navbar');
const navOffset = nav ? nav.getBoundingClientRect().height : 0;
const targetY = window.scrollY + el.getBoundingClientRect().top - navOffset - 16;
window.scrollTo({ top: Math.max(targetY, 0), behavior: 'instant' });
}
// Thumbnails are fetched by the browser as plain <img> loads, after the
// text answer and links are already on screen - not blocking on them.
// /og-image 204s (or errors) when a page has no og:image; onerror just
// hides that thumbnail rather than showing a broken image or an error.
function renderSources(sourcesEl, sources) {
sourcesEl.textContent = '';
sources.forEach(function(s, i) {
if (i > 0) sourcesEl.appendChild(document.createTextNode(' \u00b7 '));
const link = document.createElement('a');
link.href = s.url;
link.target = '_blank';
link.rel = 'noopener';
link.style.cssText = 'color:#666;display:inline-flex;align-items:center;gap:0.4rem;text-decoration:none;vertical-align:middle;';
const thumb = document.createElement('img');
thumb.src = OG_IMAGE_ENDPOINT + '?url=' + encodeURIComponent(s.url);
thumb.alt = '';
thumb.style.cssText = 'width:32px;height:32px;object-fit:cover;border-radius:4px;flex-shrink:0;';
thumb.onerror = function() { thumb.style.display = 'none'; };
link.appendChild(thumb);
const label = document.createElement('span');
label.style.textDecoration = 'underline';
label.textContent = s.title;
link.appendChild(label);
sourcesEl.appendChild(link);
});
}
async function askDave() {
const question = input.value.trim();
if (!question) return;
messages.push({ role: 'user', content: question });
appendUserMessage(question);
input.value = '';
resizeInput();
input.disabled = true;
btn.disabled = true;
btn.textContent = 'Untangling...';
const turn = appendAssistantMessage('Untangling...');
try {
const res = await fetch(DAVE_ENDPOINT, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ messages: messages })
});
if (!res.ok) {
throw new Error('Request failed: ' + res.status);
}
const data = await res.json();
const answer = data.answer || 'No answer returned.';
messages.push({ role: 'assistant', content: answer });
turn.answerEl.textContent = answer;
if (data.sources && data.sources.length) {
renderSources(turn.sourcesEl, data.sources);
}
scrollToAnswer(turn.wrap);
} catch (err) {
turn.answerEl.textContent = 'Something went wrong reaching Dave. Try again in a moment.';
messages.pop(); // don't keep a failed turn in the thread sent next time
console.error('Dave chat error:', err);
scrollToAnswer(turn.wrap);
} finally {
input.disabled = false;
btn.disabled = false;
btn.textContent = 'Ask';
input.focus({ preventScroll: true });
}
}
// Auto-expands the textarea to fit its content, up to max-height (set in
// its inline style), beyond which it scrolls internally rather than
// growing further. Resetting height to 'auto' first (before reading
// scrollHeight) is necessary so the box can shrink back down too, not
// just grow - scrollHeight alone only ever reports the larger of the two.
function resizeInput() {
input.style.height = 'auto';
input.style.height = input.scrollHeight + 'px';
}
btn.addEventListener('click', askDave);
input.addEventListener('input', resizeInput);
input.addEventListener('keydown', function(e) {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
askDave();
}
});
})();
</script>