Files
dgtlmoon 58d15ed385 WIP
2026-02-23 21:27:56 +01:00

188 lines
8.8 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* llm.js — LLM Connections management (settings page)
* Depends on: jQuery (global), LLM_CONNECTIONS + LLM_I18N injected by Jinja2 template.
*/
(function ($) {
'use strict';
// Provider presets: [value, label, model, api_base, tpm]
// tpm = tokens-per-minute limit (0 = unlimited / local).
// Defaults reflect free-tier or conservative tier-1 limits.
var LLM_PRESETS = [
['openai-mini', 'OpenAI — gpt-4o-mini', 'gpt-4o-mini', '', 200000],
['openai-4o', 'OpenAI — gpt-4o', 'gpt-4o', '', 30000],
['anthropic-haiku', 'Anthropic — claude-3-haiku', 'anthropic/claude-3-haiku-20240307', '', 100000],
['anthropic-sonnet', 'Anthropic — claude-3-5-sonnet', 'anthropic/claude-3-5-sonnet-20241022', '', 40000],
['groq-8b', 'Groq — llama-3.1-8b-instant', 'groq/llama-3.1-8b-instant', '', 6000],
['groq-70b', 'Groq — llama-3.3-70b-versatile', 'groq/llama-3.3-70b-versatile', '', 6000],
['gemini-flash', 'Google — gemini-1.5-flash', 'gemini/gemini-1.5-flash', '', 1000000],
['mistral-small', 'Mistral — mistral-small', 'mistral/mistral-small-latest', '', 500000],
['deepseek', 'DeepSeek — deepseek-chat', 'deepseek/deepseek-chat', '', 50000],
['openrouter', 'OpenRouter (custom model)', 'openrouter/', '', 20000],
['ollama-llama', 'Ollama — llama3.1 (local)', 'ollama/llama3.1', 'http://localhost:11434', 0],
['ollama-mistral', 'Ollama — mistral (local)', 'ollama/mistral', 'http://localhost:11434', 0],
['lmstudio', 'LM Studio (local)', 'openai/local', 'http://localhost:1234/v1', 0],
];
var presetMap = {};
$.each(LLM_PRESETS, function (_, p) { presetMap[p[0]] = p; });
function escHtml(s) {
return $('<div>').text(String(s)).html();
}
function maskKey(k) {
if (!k) return '<span style="color:var(--color-grey-700)">—</span>';
return escHtml(k.substring(0, 4)) + '••••';
}
// Emit WTForms FieldList hidden inputs (llm_connection-N-fieldname) so the
// server processes connections through the declared schema — no arbitrary keys.
function serialise() {
var $form = $('form.settings');
$form.find('input[data-llm-gen]').remove();
var ids = Object.keys(LLM_CONNECTIONS);
$.each(ids, function (i, id) {
var c = LLM_CONNECTIONS[id];
var prefix = 'llm_connection-' + i + '-';
var fields = {
connection_id: id,
name: c.name || '',
model: c.model || '',
api_key: c.api_key || '',
api_base: c.api_base || '',
tokens_per_minute: parseInt(c.tokens_per_minute || 0, 10)
};
$.each(fields, function (field, value) {
$('<input>').attr({ type: 'hidden', name: prefix + field, value: value, 'data-llm-gen': '1' }).appendTo($form);
});
// BooleanField: only emit when true (absence == false in WTForms)
if (c.is_default) {
$('<input>').attr({ type: 'hidden', name: prefix + 'is_default', value: 'y', 'data-llm-gen': '1' }).appendTo($form);
}
});
}
function renderTable() {
var $tbody = $('#llm-connections-tbody');
$tbody.empty();
var ids = Object.keys(LLM_CONNECTIONS);
if (!ids.length) {
$tbody.html('<tr class="llm-empty"><td colspan="6">' + escHtml(LLM_I18N.noConnections) + '</td></tr>');
return;
}
$.each(ids, function (_, id) {
var c = LLM_CONNECTIONS[id];
var tpm = parseInt(c.tokens_per_minute || 0, 10);
var tpmLabel = tpm ? tpm.toLocaleString() : '<span style="color:var(--color-grey-700)">∞</span>';
$tbody.append(
'<tr>' +
'<td class="llm-col-def">' +
'<input type="radio" class="llm-default-radio" name="llm_default_radio"' +
' title="' + escHtml(LLM_I18N.setDefault) + '"' +
(c.is_default ? ' checked' : '') +
' data-id="' + escHtml(id) + '">' +
'</td>' +
'<td class="llm-col-name">' + escHtml(c.name) + '</td>' +
'<td class="llm-col-model">' + escHtml(c.model) + '</td>' +
'<td class="llm-col-key">' + maskKey(c.api_key) + '</td>' +
'<td class="llm-col-tpm">' + tpmLabel + '</td>' +
'<td class="llm-col-del">' +
'<button type="button" class="llm-del"' +
' title="' + escHtml(LLM_I18N.remove) + '"' +
' data-id="' + escHtml(id) + '">×</button>' +
'</td>' +
'</tr>'
);
});
}
$(function () {
// Event delegation on tbody — survives re-renders
$('#llm-connections-tbody')
.on('change', '.llm-default-radio', function () {
var chosen = String($(this).data('id'));
$.each(LLM_CONNECTIONS, function (k) {
LLM_CONNECTIONS[k].is_default = (k === chosen);
});
serialise();
})
.on('click', '.llm-del', function () {
var id = String($(this).data('id'));
delete LLM_CONNECTIONS[id];
var remaining = Object.keys(LLM_CONNECTIONS);
if (remaining.length && !remaining.some(function (k) { return LLM_CONNECTIONS[k].is_default; })) {
LLM_CONNECTIONS[remaining[0]].is_default = true;
}
renderTable();
serialise();
});
function updateBaseVisibility() {
var val = $('#llm-preset').val();
var preset = presetMap[val];
var hasBase = preset ? !!preset[3] : (val === 'custom');
var show = (val === 'custom') || hasBase;
$('#llm-base-group').toggle(show);
}
// Preset dropdown pre-fills add form
$('#llm-preset').on('change', function () {
var val = $(this).val();
var p = presetMap[val];
if (p) {
$('#llm-add-name').val(p[1].replace(/\s*—.*/, '').trim());
$('#llm-add-model').val(p[2]);
$('#llm-add-base').val(p[3]);
$('#llm-add-tpm').val(p[4] !== undefined ? p[4] : 0);
$('#llm-add-key').val('');
}
updateBaseVisibility();
});
// Add connection
$('#llm-btn-add').on('click', function () {
var name = $.trim($('#llm-add-name').val());
var model = $.trim($('#llm-add-model').val());
var key = $.trim($('#llm-add-key').val());
var base = $.trim($('#llm-add-base').val());
var tpm = parseInt($('#llm-add-tpm').val(), 10) || 0;
if (!name || !model) {
alert(LLM_I18N.nameModelRequired);
return;
}
var id = 'llm-' + Date.now();
var isFirst = !Object.keys(LLM_CONNECTIONS).length;
LLM_CONNECTIONS[id] = {
name: name, model: model, api_key: key, api_base: base,
tokens_per_minute: tpm, is_default: isFirst
};
$('#llm-preset, #llm-add-name, #llm-add-model, #llm-add-key, #llm-add-base').val('');
$('#llm-add-tpm').val('0');
$('#llm-base-group').hide();
renderTable();
serialise();
});
// Show/hide API key visibility
$('#llm-key-toggle').on('click', function () {
var $inp = $('#llm-add-key');
if ($inp.attr('type') === 'password') {
$inp.attr('type', 'text');
$(this).text(LLM_I18N.hide);
} else {
$inp.attr('type', 'password');
$(this).text(LLM_I18N.show);
}
});
// Serialise connections to hidden field before form submit
$('form.settings').on('submit', serialise);
// Init
renderTable();
serialise();
});
}(jQuery));