mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-17 09:31:04 +00:00
- Implemented a new SPA page for LLM Bridge and MCP Server settings in `llm-config.js`. - Added functionality for managing LLM and MCP configurations, including toggling, saving settings, and testing connections. - Created HTTP endpoints in `llm_utils.py` for handling LLM chat, status checks, and MCP server configuration. - Integrated model fetching from LaRuche and Ollama backends. - Enhanced error handling and logging for better debugging and user feedback.
254 lines
8.5 KiB
JavaScript
254 lines
8.5 KiB
JavaScript
/**
|
|
* llm-chat — LLM Chat SPA page
|
|
* Chat interface with LLM bridge + orchestrator reasoning log.
|
|
*/
|
|
import { ResourceTracker } from '../core/resource-tracker.js';
|
|
import { api } from '../core/api.js';
|
|
import { el, $, empty, escapeHtml } from '../core/dom.js';
|
|
import { t } from '../core/i18n.js';
|
|
|
|
const PAGE = 'llm-chat';
|
|
|
|
/* ── State ─────────────────────────────────────────────── */
|
|
|
|
let tracker = null;
|
|
let root = null;
|
|
let llmEnabled = false;
|
|
let orchMode = false;
|
|
const sessionId = 'chat-' + Math.random().toString(36).slice(2, 8);
|
|
|
|
/* ── Lifecycle ─────────────────────────────────────────── */
|
|
|
|
export async function mount(container) {
|
|
tracker = new ResourceTracker(PAGE);
|
|
root = buildShell();
|
|
container.appendChild(root);
|
|
bindEvents();
|
|
await checkStatus();
|
|
sysMsg(t('llm_chat.session_started'));
|
|
}
|
|
|
|
export function unmount() {
|
|
if (tracker) { tracker.cleanupAll(); tracker = null; }
|
|
root = null;
|
|
llmEnabled = false;
|
|
orchMode = false;
|
|
}
|
|
|
|
/* ── Shell ─────────────────────────────────────────────── */
|
|
|
|
function buildShell() {
|
|
return el('div', { class: 'llmc-page' }, [
|
|
|
|
/* Header */
|
|
el('div', { class: 'llmc-header' }, [
|
|
el('span', { class: 'llmc-dot', id: 'llmc-dot' }),
|
|
el('span', { class: 'llmc-title' }, ['BJORN / CHAT']),
|
|
el('span', { class: 'llmc-status', id: 'llmc-status' }, [t('llm_chat.checking')]),
|
|
el('button', { class: 'llmc-btn-ghost', id: 'llmc-orch-btn', title: t('llm_chat.orch_title') },
|
|
[t('llm_chat.orch_log')]),
|
|
el('button', { class: 'llmc-btn-ghost llmc-clear-btn', id: 'llmc-clear-btn' },
|
|
[t('llm_chat.clear_history')]),
|
|
el('button', { class: 'llmc-btn-ghost', id: 'llmc-cfg-btn', title: 'LLM Settings' },
|
|
['\u2699']),
|
|
]),
|
|
|
|
/* Messages */
|
|
el('div', { class: 'llmc-messages', id: 'llmc-messages' }, [
|
|
el('div', { class: 'llmc-disabled-msg', id: 'llmc-disabled-msg', style: 'display:none' }, [
|
|
t('llm_chat.disabled_msg') + ' ',
|
|
el('a', { href: '#/llm-config' }, [t('llm_chat.settings_link')]),
|
|
'.',
|
|
]),
|
|
]),
|
|
|
|
/* Thinking */
|
|
el('div', { class: 'llmc-thinking', id: 'llmc-thinking', style: 'display:none' }, [
|
|
'▌ ', t('llm_chat.thinking'),
|
|
]),
|
|
|
|
/* Input row */
|
|
el('div', { class: 'llmc-input-row', id: 'llmc-input-row' }, [
|
|
el('textarea', {
|
|
class: 'llmc-input', id: 'llmc-input',
|
|
placeholder: t('llm_chat.placeholder'),
|
|
rows: '1',
|
|
}),
|
|
el('button', { class: 'llmc-send-btn', id: 'llmc-send-btn' }, [t('llm_chat.send')]),
|
|
]),
|
|
]);
|
|
}
|
|
|
|
/* ── Events ────────────────────────────────────────────── */
|
|
|
|
function bindEvents() {
|
|
const sendBtn = $('#llmc-send-btn', root);
|
|
const clearBtn = $('#llmc-clear-btn', root);
|
|
const orchBtn = $('#llmc-orch-btn', root);
|
|
const input = $('#llmc-input', root);
|
|
|
|
const cfgBtn = $('#llmc-cfg-btn', root);
|
|
|
|
if (sendBtn) tracker.on(sendBtn, 'click', send);
|
|
if (clearBtn) tracker.on(clearBtn, 'click', clearHistory);
|
|
if (orchBtn) tracker.on(orchBtn, 'click', toggleOrchLog);
|
|
if (cfgBtn) tracker.on(cfgBtn, 'click', () => { window.location.hash = '#/llm-config'; });
|
|
|
|
if (input) {
|
|
tracker.on(input, 'keydown', (e) => {
|
|
// Auto-resize
|
|
input.style.height = 'auto';
|
|
input.style.height = Math.min(input.scrollHeight, 120) + 'px';
|
|
if (e.key === 'Enter' && !e.shiftKey) {
|
|
e.preventDefault();
|
|
send();
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/* ── Status ────────────────────────────────────────────── */
|
|
|
|
async function checkStatus() {
|
|
try {
|
|
const data = await api.get('/api/llm/status', { timeout: 5000, retries: 0 });
|
|
if (!data) throw new Error('no data');
|
|
|
|
llmEnabled = data.enabled === true;
|
|
const dot = $('#llmc-dot', root);
|
|
const status = $('#llmc-status', root);
|
|
const disMsg = $('#llmc-disabled-msg', root);
|
|
const sendBtn = $('#llmc-send-btn', root);
|
|
|
|
if (!llmEnabled) {
|
|
if (dot) dot.className = 'llmc-dot offline';
|
|
if (status) status.textContent = t('llm_chat.disabled');
|
|
if (disMsg) disMsg.style.display = '';
|
|
if (sendBtn) sendBtn.disabled = true;
|
|
} else {
|
|
if (dot) dot.className = 'llmc-dot online';
|
|
const backend = data.laruche_url
|
|
? 'LaRuche @ ' + data.laruche_url
|
|
: (data.backend || 'auto');
|
|
if (status) status.textContent = t('llm_chat.online') + ' · ' + backend;
|
|
if (disMsg) disMsg.style.display = 'none';
|
|
if (sendBtn) sendBtn.disabled = false;
|
|
}
|
|
} catch {
|
|
const status = $('#llmc-status', root);
|
|
if (status) status.textContent = t('llm_chat.unavailable');
|
|
}
|
|
}
|
|
|
|
/* ── Chat ──────────────────────────────────────────────── */
|
|
|
|
async function send() {
|
|
const input = $('#llmc-input', root);
|
|
const sendBtn = $('#llmc-send-btn', root);
|
|
if (!input) return;
|
|
|
|
const msg = input.value.trim();
|
|
if (!msg) return;
|
|
input.value = '';
|
|
input.style.height = '44px';
|
|
|
|
appendMsg('user', msg);
|
|
setThinking(true);
|
|
if (sendBtn) sendBtn.disabled = true;
|
|
|
|
try {
|
|
const data = await api.post('/api/llm/chat', { message: msg, session_id: sessionId });
|
|
setThinking(false);
|
|
if (data?.status === 'ok') {
|
|
appendMsg('assistant', data.response);
|
|
} else {
|
|
sysMsg(t('llm_chat.error') + ': ' + (data?.message || 'unknown'));
|
|
}
|
|
} catch (e) {
|
|
setThinking(false);
|
|
sysMsg(t('llm_chat.net_error') + ': ' + e.message);
|
|
} finally {
|
|
if (sendBtn) sendBtn.disabled = !llmEnabled;
|
|
}
|
|
}
|
|
|
|
async function clearHistory() {
|
|
await api.post('/api/llm/clear_history', { session_id: sessionId });
|
|
const msgs = $('#llmc-messages', root);
|
|
if (!msgs) return;
|
|
empty(msgs);
|
|
const disMsg = $('#llmc-disabled-msg', root);
|
|
if (disMsg) msgs.appendChild(disMsg);
|
|
sysMsg(t('llm_chat.history_cleared'));
|
|
}
|
|
|
|
/* ── Orch log ──────────────────────────────────────────── */
|
|
|
|
async function toggleOrchLog() {
|
|
orchMode = !orchMode;
|
|
const orchBtn = $('#llmc-orch-btn', root);
|
|
const inputRow = $('#llmc-input-row', root);
|
|
const msgs = $('#llmc-messages', root);
|
|
|
|
if (orchMode) {
|
|
if (orchBtn) { orchBtn.classList.add('active'); orchBtn.textContent = t('llm_chat.back_chat'); }
|
|
if (inputRow) inputRow.style.display = 'none';
|
|
if (msgs) empty(msgs);
|
|
await loadOrchLog();
|
|
} else {
|
|
if (orchBtn) { orchBtn.classList.remove('active'); orchBtn.textContent = t('llm_chat.orch_log'); }
|
|
if (inputRow) inputRow.style.display = '';
|
|
if (msgs) empty(msgs);
|
|
sysMsg(t('llm_chat.back_to_chat'));
|
|
}
|
|
}
|
|
|
|
async function loadOrchLog() {
|
|
sysMsg(t('llm_chat.loading_log'));
|
|
try {
|
|
const data = await api.get('/api/llm/reasoning', { timeout: 10000, retries: 0 });
|
|
const msgs = $('#llmc-messages', root);
|
|
if (!msgs) return;
|
|
empty(msgs);
|
|
|
|
if (!data?.messages?.length) {
|
|
sysMsg(t('llm_chat.no_log'));
|
|
return;
|
|
}
|
|
sysMsg(t('llm_chat.log_header') + ' — ' + data.count + ' message(s)');
|
|
for (const m of data.messages) {
|
|
appendMsg(m.role === 'user' ? 'user' : 'assistant', m.content || '');
|
|
}
|
|
} catch (e) {
|
|
sysMsg(t('llm_chat.log_error') + ': ' + e.message);
|
|
}
|
|
}
|
|
|
|
/* ── Helpers ───────────────────────────────────────────── */
|
|
|
|
function appendMsg(role, text) {
|
|
const msgs = $('#llmc-messages', root);
|
|
if (!msgs) return;
|
|
const labels = { user: 'YOU', assistant: 'BJORN' };
|
|
const roleLabel = labels[role] || role.toUpperCase();
|
|
const div = el('div', { class: 'llmc-msg ' + role }, [
|
|
el('div', { class: 'llmc-msg-role' }, [roleLabel]),
|
|
document.createTextNode(text),
|
|
]);
|
|
msgs.appendChild(div);
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
}
|
|
|
|
function sysMsg(text) {
|
|
const msgs = $('#llmc-messages', root);
|
|
if (!msgs) return;
|
|
const div = el('div', { class: 'llmc-msg system' }, [text]);
|
|
msgs.appendChild(div);
|
|
msgs.scrollTop = msgs.scrollHeight;
|
|
}
|
|
|
|
function setThinking(on) {
|
|
const el = $('#llmc-thinking', root);
|
|
if (el) el.style.display = on ? '' : 'none';
|
|
}
|