Samme indhold, URL'er og udseende, bygget af en enkelt zola-binær og Tailwind standalone-CLI uden Node. Kanonisk domæne er asger.weirsoe.dk. Blogindlæg ligger som page bundles med frontmatter Zola forstår, så de gamle stempler er ugyldige, indtil scripts/omstempl.py er kørt med PUBLISH_KEY. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
65 lines
3.5 KiB
HTML
65 lines
3.5 KiB
HTML
<div id="chat-log" class="mt-8 space-y-3" aria-live="polite"></div>
|
|
|
|
<form id="chat-form" class="mt-6 flex gap-3" data-thinking="{% if lang == "en" %}…thinking{% else %}…tænker{% endif %}" data-offline="{% if lang == "en" %}The chat is offline right now.. try again later.{% else %}Chatten er offline lige nu.. prøv senere.{% endif %}" data-unreachable="{% if lang == "en" %}Could not reach the chat.. try again in a moment.{% else %}Kunne ikke nå chatten.. prøv igen om lidt.{% endif %}">
|
|
<input id="chat-input" type="text" required maxlength="500" autocomplete="off"
|
|
placeholder="{% if lang == "en" %}Ask about experience, incident response, Peppol…{% else %}Spørg om erfaring, Peppol, homelab…{% endif %}"
|
|
class="flex-1 wt-input wt-input--lg" />
|
|
<button type="submit" class="wt-btn wt-btn--primary wt-btn--lg">Send</button>
|
|
</form>
|
|
<p class="mt-3 text-xs text-muted/80">{% if lang == "en" %}Answers are written by Claude (Anthropic) from my public CV.. check important details with me directly.{% else %}Svarene skrives af Claude (Anthropic) ud fra mit offentlige CV.. tjek vigtige detaljer med mig direkte.{% endif %}</p>
|
|
|
|
<script>
|
|
(function () {
|
|
var form = document.getElementById('chat-form');
|
|
if (!form || form.__wired) return; form.__wired = true;
|
|
var history = [];
|
|
function esc(s) {
|
|
return s.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
}
|
|
function linkify(text) {
|
|
var html = esc(text);
|
|
html = html.replace(/\b(https?:\/\/[^\s<]+[^\s<.,!?:;)])/g,
|
|
'<a href="$1" target="_blank" rel="noopener" class="text-accent underline">$1</a>');
|
|
html = html.replace(/\b([A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,})\b/g,
|
|
'<a href="mailto:$1" class="text-accent underline">$1</a>');
|
|
return html;
|
|
}
|
|
function track(action, q) {
|
|
try { (window._paq = window._paq || []).push(['trackEvent', 'Chat', action, q]); } catch (_) {}
|
|
}
|
|
function bubble(role, text) {
|
|
var log = document.getElementById('chat-log');
|
|
var d = document.createElement('div');
|
|
d.className = 'wt-card text-sm whitespace-pre-wrap ' + (role === 'user' ? 'wt-card--alt ml-8' : 'mr-8');
|
|
if (role === 'user') d.textContent = text; else d.innerHTML = linkify(text);
|
|
log.appendChild(d);
|
|
d.scrollIntoView({ behavior: 'smooth', block: 'end' });
|
|
return d;
|
|
}
|
|
form.addEventListener('submit', async function (e) {
|
|
e.preventDefault();
|
|
var input = document.getElementById('chat-input');
|
|
var q = input.value.trim(); if (!q) return;
|
|
input.value = '';
|
|
bubble('user', q);
|
|
history.push({ role: 'user', content: q });
|
|
var wait = bubble('assistant', form.dataset.thinking);
|
|
var btn = form.querySelector('button'); btn.disabled = true;
|
|
try {
|
|
var r = await fetch('/api/chat', {
|
|
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ messages: history.slice(-10) }),
|
|
});
|
|
var data = await r.json();
|
|
if (!r.ok) { wait.textContent = data.error || form.dataset.offline; track('afvist', q); return; }
|
|
wait.innerHTML = linkify(data.reply);
|
|
history.push({ role: 'assistant', content: data.reply });
|
|
track('besvaret', q);
|
|
} catch (_) {
|
|
wait.textContent = form.dataset.unreachable;
|
|
track('fejl', q);
|
|
} finally { btn.disabled = false; input.focus(); }
|
|
});
|
|
})();
|
|
</script>
|