LLM:
(function() {
/* ============================================================
HELIKON CHAT - MULTI-LLM TEMPLATE
============================================================
Page Properties needed:
buddy.apiurl = Local/BWO LLM endpoint (OpenAI-compatible)
buddy.apikey = Local/BWO API key
buddy.apiurl2 = Claude API endpoint (https://api.anthropic.com/v1/messages)
buddy.apikey2 = Claude/Anthropic API key
buddy.model = Local model name (default: 'default')
buddy.model2 = Claude model name (default: 'claude-sonnet-4-20250514')
Files needed:
/@api/deki/files/404/=system-prompt.txt
============================================================ */
/* === CONFIG (loaded from page properties) === */
var PROMPT_URL = '/@api/deki/files/404/=system-prompt.txt';
var SHORT_LEN = 100;
var SEARCH_FIRST = true;
/* === PROVIDER STATE === */
var providers = {
p1: { url: '', key: '', model: 'default', name: 'Local', type: 'openai', ready: false },
p2: { url: '', key: '', model: 'claude-sonnet-4-20250514', name: 'Claude', type: 'anthropic', ready: false }
};
var activeProvider = 'p1';
var dualMode = false;
/* === DOM === */
var log = document.getElementById('chatLog');
var input = document.getElementById('chatInput');
var btn = document.getElementById('chatSend');
var status = document.getElementById('chatStatus');
var avatar = document.getElementById('buddyChatImg');
var bubble = document.getElementById('buddyBubble');
var exportBtn = document.getElementById('chatExport');
var llmBtn1 = document.getElementById('llmBtn1');
var llmBtn2 = document.getElementById('llmBtn2');
var llmBtnDual = document.getElementById('llmBtnDual');
if (!log || !input || !btn) return;
log.innerHTML = '';
/* === INJECT CSS === */
var T = String.fromCharCode(60);
var TC = String.fromCharCode(60, 47);
var G = String.fromCharCode(62);
var css = ''
+ '#serverBuddy { display: none !important; }'
+ '#buddySpeech { display: none !important; }'
+ '#buddyChat { display:flex; gap:24px; align-items:flex-start; width:100%; font-family:"Courier New",Courier,monospace; color:#333; }'
+ '#buddyChatMain { flex:1; min-width:0; }'
+ '#buddyChatSide { width:280px; flex-shrink:0; display:flex; flex-direction:column; align-items:center; position:sticky; top:40px; }'
+ '#buddyChatSide img { width:280px; height:280px; image-rendering:pixelated; image-rendering:crisp-edges; display:block; border:none; }'
+ '#buddyBubble { transform:scale(0); background:#fff; color:#333; padding:12px 18px; border-radius:14px; border:2px solid #ccc; font-size:15px; line-height:1.4; max-width:280px; text-align:center; white-space:pre-wrap; word-wrap:break-word; opacity:0; transition:opacity 0.3s ease,transform 0.3s ease; box-shadow:0 2px 8px rgba(0,0,0,0.1); margin-bottom:12px; }'
+ '#buddyBubble.visible { opacity:1; transform:scale(1); }'
+ '#buddyBubble::after { content:""; position:absolute; bottom:-10px; left:50%; transform:translateX(-50%); width:0; height:0; border-left:10px solid transparent; border-right:10px solid transparent; border-top:10px solid #ccc; }'
+ '#chatLog { min-height:400px; max-height:600px; overflow-y:auto; padding:20px; margin-bottom:16px; border:2px solid #ccc; background:#f8f8f8; }'
+ '.chatMsg { margin-bottom:14px; padding:12px 16px; border-radius:8px; font-size:16px; line-height:1.6; max-width:90%; word-wrap:break-word; }'
+ '.chatMsg.user { background:#e8e8e8; color:#555; margin-left:auto; text-align:right; border:1px solid #ddd; }'
+ '.chatMsg.buddy { background:#fff; color:#333; margin-right:auto; border:1px solid #ccc; }'
+ '.chatMsg.buddy::before { content:"> "; color:#0077aa; margin-right:4px; opacity:0.6; }'
+ '.chatMsg.buddy.provider-p2 { border-left:3px solid #d97706; }'
+ '.chatMsg.buddy.provider-p2::before { content:"> "; color:#d97706; margin-right:4px; opacity:0.6; }'
+ '.chatMsg .provider-tag { font-size:10px; opacity:0.5; display:block; margin-bottom:4px; text-transform:uppercase; letter-spacing:1px; }'
+ '.chatMsg.system { color:#999; font-size:13px; text-align:center; margin:8px auto; max-width:100%; }'
+ '.chatMsg.typing { color:#999; opacity:0.6; }'
+ '.chatMsg a { color:#0077aa; text-decoration:underline; }'
+ '.chatMsg a:hover { color:#005580; }'
+ '#chatInputWrap { display:flex; gap:8px; border:2px solid #ccc; background:#f8f8f8; padding:10px; }'
+ '#chatInput { flex:1; background:#fff; border:1px solid #ccc; color:#333; font-family:"Courier New",Courier,monospace; font-size:16px; padding:12px 14px; outline:none; }'
+ '#chatInput::placeholder { color:#aaa; }'
+ '#chatInput:focus { border-color:#0077aa; }'
+ '#chatSend { background:#fff; color:#0077aa; border:1px solid #0077aa; padding:10px 20px; font-family:"Courier New",Courier,monospace; font-size:14px; cursor:pointer; }'
+ '#chatSend:hover { background:#0077aa; color:#fff; }'
+ '#chatSend:disabled { opacity:0.3; cursor:default; }'
+ '#llmSwitch { display:flex; align-items:center; gap:6px; margin-top:8px; padding:6px 0; }'
+ '#llmLabel { font-size:11px; color:#999; margin-right:4px; }'
+ '.llmBtn { background:#f0f0f0; color:#666; border:1px solid #ccc; padding:4px 12px; font-family:"Courier New",Courier,monospace; font-size:11px; cursor:pointer; border-radius:3px; transition:all 0.2s ease; }'
+ '.llmBtn:hover { border-color:#0077aa; color:#0077aa; }'
+ '.llmBtn.active { background:#0077aa; color:#fff; border-color:#0077aa; }'
+ '.llmBtn.active-dual { background:#d97706; color:#fff; border-color:#d97706; }'
+ '.llmBtn:disabled { opacity:0.3; cursor:default; }'
+ '#chatFooter { display:flex; justify-content:space-between; align-items:center; margin-top:4px; }'
+ '#chatStatus { font-size:11px; color:#999; }'
+ '#chatExport { background:none; border:1px solid #ccc; color:#999; padding:3px 10px; font-size:11px; cursor:pointer; font-family:"Courier New",Courier,monospace; }'
+ '#chatExport:hover { border-color:#0077aa; color:#0077aa; }'
+ '@media (max-width:768px) {'
+ ' #buddyChat { flex-direction:column-reverse; }'
+ ' #buddyChatSide { width:100%; flex-direction:row; justify-content:center; position:static; gap:12px; }'
+ ' #buddyChatSide img { width:96px; height:96px; }'
+ ' #buddyBubble { max-width:200px; font-size:12px; margin-bottom:0; }'
+ '}';
var styleEl = document.createElement('style');
styleEl.textContent = css;
document.head.appendChild(styleEl);
/* === GIFs === */
var GIFS = new Object();
GIFS.idle = '/@api/deki/files/403/=buddy_idle.gif';
GIFS.sleep = '/@api/deki/files/402/=buddy_sleeping.gif';
GIFS.happy = '/@api/deki/files/401/=buddy_happy.gif';
GIFS.error = '/@api/deki/files/400/=buddy_error.gif';
GIFS.loading = '/@api/deki/files/399/=buddy_loading.gif';
/* === STATE === */
var history = [];
var busy = false;
var sysMsg = '';
var bubbleTimer = null;
var pendingDual = 0;
/* === GET PAGE ID === */
var pageId = 0;
if (typeof Deki !== 'undefined' && Deki.PageId) {
pageId = Deki.PageId;
}
/* === HELPERS === */
function setGif(name) { if (avatar) avatar.src = GIFS[name] || GIFS.idle; }
function setStatus(txt) { if (status) status.textContent = txt; }
function showBubble(txt, dur) {
if (!bubble) return;
bubble.textContent = txt;
bubble.className = 'visible';
if (bubbleTimer) clearTimeout(bubbleTimer);
bubbleTimer = setTimeout(function() { bubble.className = ''; }, dur || 4000);
}
function addMsg(txt, cls, providerKey) {
var div = document.createElement('div');
div.className = 'chatMsg ' + cls;
if (providerKey) {
div.className += ' provider-' + providerKey;
}
if (dualMode && providerKey && cls === 'buddy') {
var tag = document.createElement('span');
tag.className = 'provider-tag';
tag.textContent = providers[providerKey] ? providers[providerKey].name : providerKey;
div.appendChild(tag);
}
var textNode = document.createTextNode(txt);
div.appendChild(textNode);
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
function addTyping(providerKey) {
var div = document.createElement('div');
div.className = 'chatMsg buddy typing';
if (providerKey) {
div.className += ' typing-' + providerKey;
}
div.textContent = '. . .';
log.appendChild(div);
log.scrollTop = log.scrollHeight;
}
function removeTyping(providerKey) {
var selector = providerKey ? '.typing-' + providerKey : '.typing';
var els = log.querySelectorAll(selector);
var i;
for (i = 0; i < els.length; i++) {
log.removeChild(els[i]);
}
}
/* === FETCH HELPERS === */
function fetchText(url, callback) {
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
callback(xhr.status === 200 ? xhr.responseText.replace(/^\s+|\s+$/g, '') : '');
};
xhr.onerror = function() { callback(''); };
xhr.send();
}
function fetchAllProperties(callback) {
if (!pageId) { callback({}); return; }
var url = '/@api/deki/pages/' + pageId + '/properties';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
var props = {};
if (xhr.status === 200) {
try {
var parser = new DOMParser();
var xml = parser.parseFromString(xhr.responseText, 'text/xml');
var nodes = xml.getElementsByTagName('property');
var i;
for (i = 0; i < nodes.length; i++) {
var nameAttr = nodes[i].getAttribute('name') || '';
var shortName = nameAttr;
var dotPos = nameAttr.lastIndexOf('#');
if (dotPos !== -1) shortName = nameAttr.substring(dotPos + 1);
var contents = nodes[i].getElementsByTagName('contents');
if (contents.length > 0 && contents[0].textContent) {
props[shortName] = contents[0].textContent.replace(/^\s+|\s+$/g, '');
}
}
} catch (e) {}
}
callback(props);
};
xhr.onerror = function() { callback({}); };
xhr.send();
}
/* === MINDTOUCH SEARCH === */
function searchMindTouch(query, callback) {
if (!SEARCH_FIRST) { callback(null); return; }
var url = '/@api/deki/site/search?q=' + encodeURIComponent(query) + String.fromCharCode(38) + 'limit=5';
var xhr = new XMLHttpRequest();
xhr.open('GET', url, true);
xhr.onload = function() {
if (xhr.status !== 200) { callback(null); return; }
try {
var parser = new DOMParser();
var xml = parser.parseFromString(xhr.responseText, 'text/xml');
var pages = xml.getElementsByTagName('page');
if (pages.length === 0) { callback(null); return; }
var results = [];
var i;
for (i = 0; i < pages.length && i < 5; i++) {
var titleEl = pages[i].getElementsByTagName('title');
var pathEl = pages[i].getElementsByTagName('path');
if (titleEl.length > 0 && pathEl.length > 0) {
results.push({ title: titleEl[0].textContent, path: '/' + pathEl[0].textContent });
}
}
if (results.length === 0) { callback(null); return; }
var ctx = '\n\n--- Suchergebnisse aus der Dokumentation ---\n';
var ri;
for (ri = 0; ri < results.length; ri++) {
ctx += (ri + 1) + '. ' + results[ri].title + ' (' + results[ri].path + ')\n';
}
ctx += '---\nBitte verwende NUR diese tatsaechlichen Seiten als Referenz. Erfinde KEINE Pfade.\n';
callback(ctx);
} catch (e) { callback(null); }
};
xhr.onerror = function() { callback(null); };
xhr.send();
}
/* === LLM CALL: OpenAI-compatible (BWO/Local) === */
function callOpenAI(prov, messages, callback) {
var headers = { 'Content-Type': 'application/json' };
if (prov.key) headers['Authorization'] = 'Bearer ' + prov.key;
var body = JSON.stringify({
model: prov.model,
messages: messages,
max_tokens: 500
});
var xhr = new XMLHttpRequest();
xhr.open('POST', prov.url, true);
var key;
for (key in headers) {
if (headers.hasOwnProperty(key)) {
xhr.setRequestHeader(key, headers[key]);
}
}
xhr.onload = function() {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
callback(null, data.choices[0].message.content);
} catch (e) {
callback('Parse-Fehler');
}
} else {
callback('API-Fehler ' + xhr.status);
}
};
xhr.onerror = function() { callback('Nicht erreichbar'); };
xhr.send(body);
}
/* === LLM CALL: Anthropic (Claude) === */
function callAnthropic(prov, sysPrompt, messages, callback) {
var anthropicMessages = [];
var i;
for (i = 0; i < messages.length; i++) {
if (messages[i].role !== 'system') {
anthropicMessages.push({ role: messages[i].role, content: messages[i].content });
}
}
var body = JSON.stringify({
model: prov.model,
max_tokens: 500,
system: sysPrompt,
messages: anthropicMessages
});
var xhr = new XMLHttpRequest();
xhr.open('POST', prov.url, true);
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.setRequestHeader('x-api-key', prov.key);
xhr.setRequestHeader('anthropic-version', '2023-06-01');
xhr.setRequestHeader('anthropic-dangerous-direct-browser-access', 'true');
xhr.onload = function() {
if (xhr.status === 200) {
try {
var data = JSON.parse(xhr.responseText);
var reply = '';
var ci;
for (ci = 0; ci < data.content.length; ci++) {
if (data.content[ci].type === 'text') {
reply += data.content[ci].text;
}
}
callback(null, reply);
} catch (e) {
callback('Parse-Fehler');
}
} else {
callback('API-Fehler ' + xhr.status);
}
};
xhr.onerror = function() { callback('Nicht erreichbar'); };
xhr.send(body);
}
/* === UNIFIED LLM DISPATCH === */
function callProvider(providerKey, searchContext, callback) {
var prov = providers[providerKey];
if (!prov || !prov.ready) {
callback('Provider nicht konfiguriert');
return;
}
var augmentedSys = sysMsg;
if (searchContext) augmentedSys = sysMsg + searchContext;
var messages = [{ role: 'system', content: augmentedSys }];
var i;
for (i = 0; i < history.length; i++) {
messages.push(history[i]);
}
if (prov.type === 'anthropic') {
callAnthropic(prov, augmentedSys, messages, callback);
} else {
callOpenAI(prov, messages, callback);
}
}
/* === HANDLE REPLY === */
function handleReply(reply, providerKey) {
history.push({ role: 'assistant', content: reply });
addMsg(reply, 'buddy', providerKey);
var dot = reply.indexOf('.');
var summary;
if (dot > 0 && dot < 80) {
summary = reply.substring(0, dot + 1);
} else if (reply.length > 60) {
summary = reply.substring(0, 57) + '...';
} else {
summary = reply;
}
showBubble(summary, 6000);
setGif('happy');
setTimeout(function() { setGif('idle'); }, 3000);
}
function handleError(errMsg, providerKey) {
addMsg('Fehler (' + providers[providerKey].name + '): ' + errMsg, 'system');
showBubble('Fehler!', 3000);
setGif('error');
setTimeout(function() { setGif('idle'); }, 4000);
setStatus(providers[providerKey].name + ': ' + errMsg);
}
/* === SEND MESSAGE === */
function send() {
var msg = input.value.replace(/^\s+|\s+$/g, '');
if (!msg || busy) return;
addMsg(msg, 'user');
history.push({ role: 'user', content: msg });
input.value = '';
busy = true;
btn.disabled = true;
setGif('loading');
setStatus('denkt nach...');
searchMindTouch(msg, function(searchContext) {
if (dualMode) {
/* --- DUAL MODE: both providers answer --- */
pendingDual = 0;
var targets = ['p1', 'p2'];
var ti;
for (ti = 0; ti < targets.length; ti++) {
if (providers[targets[ti]].ready) {
pendingDual++;
addTyping(targets[ti]);
}
}
if (pendingDual === 0) {
addMsg('Kein Provider verfuegbar.', 'system');
busy = false;
btn.disabled = false;
setGif('error');
return;
}
var dualDone = function() {
pendingDual--;
if (pendingDual <= 0) {
busy = false;
btn.disabled = false;
input.focus();
setStatus('bereit');
}
};
for (ti = 0; ti < targets.length; ti++) {
(function(pk) {
if (!providers[pk].ready) return;
callProvider(pk, searchContext, function(err, reply) {
removeTyping(pk);
if (err) {
handleError(err, pk);
} else {
addMsg(reply, 'buddy', pk);
showBubble(providers[pk].name + ' hat geantwortet', 3000);
}
dualDone();
});
})(targets[ti]);
}
/* In dual mode, only store one reply in history (last one) */
} else {
/* --- SINGLE MODE --- */
addTyping(activeProvider);
callProvider(activeProvider, searchContext, function(err, reply) {
removeTyping(activeProvider);
if (err) {
handleError(err, activeProvider);
} else {
handleReply(reply, activeProvider);
setStatus(providers[activeProvider].name + ' | bereit');
}
busy = false;
btn.disabled = false;
input.focus();
});
}
});
}
/* === LLM SWITCH UI === */
function updateSwitchUI() {
llmBtn1.className = 'llmBtn';
llmBtn2.className = 'llmBtn';
llmBtnDual.className = 'llmBtn';
if (!providers.p1.ready) llmBtn1.disabled = true;
if (!providers.p2.ready) llmBtn2.disabled = true;
if (!providers.p1.ready || !providers.p2.ready) llmBtnDual.disabled = true;
if (dualMode) {
llmBtnDual.className = 'llmBtn active-dual';
} else if (activeProvider === 'p1') {
llmBtn1.className = 'llmBtn active';
} else {
llmBtn2.className = 'llmBtn active';
}
if (!dualMode && providers[activeProvider]) {
setStatus(providers[activeProvider].name + ' | bereit');
} else if (dualMode) {
setStatus('Dual Mode | bereit');
}
}
llmBtn1.addEventListener('click', function() {
if (!providers.p1.ready) return;
dualMode = false;
activeProvider = 'p1';
updateSwitchUI();
});
llmBtn2.addEventListener('click', function() {
if (!providers.p2.ready) return;
dualMode = false;
activeProvider = 'p2';
updateSwitchUI();
});
llmBtnDual.addEventListener('click', function() {
if (!providers.p1.ready || !providers.p2.ready) return;
dualMode = !dualMode;
updateSwitchUI();
});
/* === INPUT HANDLERS === */
btn.addEventListener('click', send);
input.addEventListener('keydown', function(e) {
if (e.keyCode === 13) send();
});
/* === PDF EXPORT === */
if (exportBtn) {
exportBtn.addEventListener('click', function() {
var T = String.fromCharCode(60);
var TC = String.fromCharCode(60, 47);
var G = String.fromCharCode(62);
var html = T + 'html' + G + T + 'head' + G;
html += T + 'style' + G;
html += 'body{font-family:"Courier New",monospace;padding:20px;color:#333}';
html += '.msg{margin:8px 0;padding:8px;border-radius:4px}';
html += '.user{background:#e8e8e8;text-align:right}';
html += '.buddy{background:#fff;border:1px solid #ccc}';
html += '.system{color:#999;text-align:center;font-size:11px}';
html += '.tag{font-size:9px;color:#999;text-transform:uppercase}';
html += TC + 'style' + G + TC + 'head' + G + T + 'body' + G;
html += T + 'h2' + G + 'Helikon Chat Export' + TC + 'h2' + G;
html += T + 'p' + G + new Date().toLocaleString() + TC + 'p' + G;
var msgs = log.getElementsByClassName('chatMsg');
var mi;
for (mi = 0; mi < msgs.length; mi++) {
var m = msgs[mi];
if (m.className.indexOf('typing') !== -1) continue;
var cls = 'buddy';
if (m.className.indexOf('user') !== -1) cls = 'user';
if (m.className.indexOf('system') !== -1) cls = 'system';
html += T + 'div class="msg ' + cls + '"' + G;
html += m.textContent;
html += TC + 'div' + G;
}
html += TC + 'body' + G + TC + 'html' + G;
var w = window.open('', '_blank');
if (w) {
w.document.write(html);
w.document.close();
w.print();
}
});
}
/* === INIT === */
setGif('loading');
setStatus('lade konfiguration...');
input.disabled = true;
btn.disabled = true;
llmBtn1.disabled = true;
llmBtn2.disabled = true;
llmBtnDual.disabled = true;
fetchAllProperties(function(props) {
/* Provider 1: Local/BWO */
providers.p1.url = props['buddy.apiurl'] || '';
providers.p1.key = props['buddy.apikey'] || '';
providers.p1.model = props['buddy.model'] || 'default';
providers.p1.ready = !!providers.p1.url;
if (props['buddy.name1']) providers.p1.name = props['buddy.name1'];
/* Provider 2: Claude */
providers.p2.url = props['buddy.apiurl2'] || '';
providers.p2.key = props['buddy.apikey2'] || '';
providers.p2.model = props['buddy.model2'] || 'claude-sonnet-4-20250514';
providers.p2.ready = !!providers.p2.url;
if (props['buddy.name2']) providers.p2.name = props['buddy.name2'];
/* Update button labels from config */
llmBtn1.textContent = providers.p1.name;
llmBtn2.textContent = providers.p2.name;
/* Set default active provider */
if (providers.p1.ready) {
activeProvider = 'p1';
} else if (providers.p2.ready) {
activeProvider = 'p2';
}
var statusParts = [];
statusParts.push(providers.p1.name + ': ' + (providers.p1.ready ? 'OK' : 'FEHLT'));
statusParts.push(providers.p2.name + ': ' + (providers.p2.ready ? 'OK' : 'FEHLT'));
setStatus(statusParts.join(' | '));
/* Load system prompt */
fetchText(PROMPT_URL, function(val) {
sysMsg = val || 'Du bist Helikon, ein freundlicher Dokumentations-Assistent.';
setGif('idle');
input.disabled = false;
btn.disabled = false;
updateSwitchUI();
if (providers.p1.ready || providers.p2.ready) {
showBubble('Hallo! Wie kann ich helfen?', 5000);
} else {
showBubble('Kein LLM konfiguriert!', 5000);
setGif('error');
setStatus('buddy.apiurl und buddy.apiurl2 fehlen');
}
});
});
})();