// Shared LLM module - provides streaming chat to any page var LLM = (function() { var defaultApiUrl = 'https://llama-instruct.reeselink.com/v1'; var defaultModel = 'instruct'; function getConfig() { return { apiUrl: localStorage.getItem('apiUrl') || defaultApiUrl, token: localStorage.getItem('apiToken') || '', model: localStorage.getItem('modelName') || defaultModel }; } function callAPI(messages, onChunk, onComplete, onError) { var config = getConfig(); var apiUrl = config.apiUrl.replace(/\/+$/, ''); var headers = { 'Content-Type': 'application/json' }; if (config.token) { headers['Authorization'] = 'Bearer ' + config.token; } fetch(apiUrl + '/chat/completions', { method: 'POST', headers: headers, body: JSON.stringify({ messages: messages, model: config.model, stream: true }) }) .then(function(response) { if (!response.ok) { throw new Error('API error: ' + response.status + ' ' + response.statusText); } var reader = response.body.getReader(); var decoder = new TextDecoder(); var buffer = ''; function read() { return reader.read().then(function(result) { var done = result.done; var value = result.value; if (done) { onComplete(); return; } buffer += decoder.decode(value, { stream: true }); var lines = buffer.split('\n'); buffer = lines.pop(); for (var i = 0; i < lines.length; i++) { var line = lines[i].trim(); if (line.startsWith('data: ')) { var data = line.slice(6); if (data === '[DONE]') { onComplete(); return; } try { var json = JSON.parse(data); var delta = json.choices && json.choices[0] && json.choices[0].delta; if (delta && delta.content) { onChunk(delta.content); } } catch (e) { // skip malformed JSON } } } return read(); }); } return read(); }) .catch(function(err) { onError(err.message); }); } function chat(elementId, systemPrompt, userMessage) { var messages = []; if (systemPrompt) { messages.push({ role: 'system', content: systemPrompt }); } messages.push({ role: 'user', content: userMessage }); return new Promise(function(resolve, reject) { var outputEl = document.getElementById(elementId); if (!outputEl) { reject(new Error('Output element not found')); return; } outputEl.innerHTML = 'Thinking...'; var fullText = ''; var history = systemPrompt ? messages : []; callAPI( history, function(chunk) { fullText += chunk; outputEl.innerHTML = formatMarkdown(fullText); outputEl.style.whiteSpace = 'pre-wrap'; }, function() { resolve(fullText); }, function(err) { outputEl.innerHTML = 'Error: ' + escapeHTML(err) + ''; reject(err); } ); }); } function chatWithHistory(elementId, history, onDone) { return new Promise(function(resolve, reject) { var outputEl = document.getElementById(elementId); if (!outputEl) { reject(new Error('Output element not found')); return; } outputEl.innerHTML = 'Thinking...'; var fullText = ''; callAPI( history, function(chunk) { fullText += chunk; outputEl.innerHTML = formatMarkdown(fullText); outputEl.style.whiteSpace = 'pre-wrap'; }, function() { if (onDone) onDone(); resolve(fullText); }, function(err) { outputEl.innerHTML = 'Error: ' + escapeHTML(err) + ''; reject(err); } ); }); } function escapeHTML(str) { var div = document.createElement('div'); div.textContent = str; return div.innerHTML; } function formatMarkdown(text) { // Extract code blocks first to protect them from escaping var codeBlocks = []; text = text.replace(/```([\s\S]*?)```/g, function(match, code) { var placeholder = '%%CODEBLOCK' + codeBlocks.length + '%%'; codeBlocks.push('
' + escapeHTML(code) + '');
return placeholder;
});
// Escape remaining HTML
text = escapeHTML(text);
// Restore code blocks
for (var i = 0; i < codeBlocks.length; i++) {
text = text.replace('%%CODEBLOCK' + i + '%%', codeBlocks[i]);
}
// Inline code
text = text.replace(/`([^`]+)`/g, '$1');
// Bold
text = text.replace(/\*\*([^*]+)\*\*/g, '$1');
// Line breaks
text = text.replace(/\n/g, '