mirror of
https://github.com/infinition/Bjorn.git
synced 2026-03-17 17:41:03 +00:00
Add LLM configuration and MCP server management UI and backend functionality
- 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.
This commit is contained in:
253
web/js/pages/llm-chat.js
Normal file
253
web/js/pages/llm-chat.js
Normal file
@@ -0,0 +1,253 @@
|
||||
/**
|
||||
* 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';
|
||||
}
|
||||
Reference in New Issue
Block a user