Настройка из браузера: вкладка ⚙ вместо правки файла
Инсталлятор спрашивает адрес Ollama в консоли, но это можно пропустить и задать всё с телефона: адрес, модель для разбора фраз, модель для поиска по смыслу. «Проверить связь» показывает, отвечает ли Ollama и сколько там моделей; сами модели выбираются из реально установленных, а не вводятся руками. Если Ollama молчит, предупреждение наверху ведёт прямо в настройки. Настройки применяются сразу: модули читают их через config.X, а не забирают копию при импорте, поэтому перезапуск не нужен. Из веба правятся только OLLAMA_HOST и две модели — по белому списку, потому что .env читает приложение при запуске. Адрес проверяется на формат, имена моделей — на недопустимые символы. Попутно исправлены два дефекта, найденных тестами: - .env в песочнице теперь тоже свой; до этого прогон тестов перезаписывал рабочий файл настроек; - значения проверяются все до применения: раньше неверное имя модели оставляло уже подменённый адрес Ollama. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+113
-10
@@ -42,6 +42,8 @@
|
||||
nav button { flex:1; padding:11px 4px; font-size:15px; font-weight:600; background:none;
|
||||
border:none; border-bottom:3px solid transparent; color:var(--dim); cursor:pointer; }
|
||||
nav button.on { color:var(--fg); border-bottom-color:var(--accent); }
|
||||
nav button.narrowTab { flex:0 0 48px; font-size:18px; }
|
||||
code { background:var(--card); padding:1px 5px; border-radius:5px; font-size:14px; }
|
||||
main { padding:16px; max-width:720px; margin:0 auto; }
|
||||
.tab { display:none; } .tab.on { display:block; }
|
||||
|
||||
@@ -131,6 +133,7 @@
|
||||
<button class="on" data-tab="do">Действие</button>
|
||||
<button data-tab="find">Найти</button>
|
||||
<button data-tab="tree">Обзор</button>
|
||||
<button data-tab="setup" class="narrowTab">⚙</button>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
@@ -213,6 +216,34 @@
|
||||
<div id="findCards"></div>
|
||||
</section>
|
||||
|
||||
<!-- ================= Настройки ================= -->
|
||||
<section class="tab" id="tab-setup">
|
||||
<p class="hint" id="setupIntro">Здесь настраивается связь с Ollama — это «мозги»
|
||||
помощника. Без неё всё остальное работает, но не будет разбора фраз,
|
||||
автоматических тегов и поиска по смыслу.</p>
|
||||
|
||||
<label>Адрес Ollama</label>
|
||||
<input type="text" id="cfgHost" placeholder="http://192.168.1.50:11434" autocomplete="off"
|
||||
inputmode="url">
|
||||
<p class="hint">Если Ollama на этом же компьютере — <code>http://localhost:11434</code>.
|
||||
Если на другой машине — её адрес в локальной сети.</p>
|
||||
<div class="row mt">
|
||||
<button class="act ghost" id="cfgProbe">Проверить связь</button>
|
||||
</div>
|
||||
<div id="cfgProbeResult"></div>
|
||||
|
||||
<label>Модель для разбора фраз</label>
|
||||
<select id="cfgChat"></select>
|
||||
<label>Модель для поиска по смыслу</label>
|
||||
<select id="cfgEmbed"></select>
|
||||
<p class="hint">Список заполняется моделями, которые реально установлены.
|
||||
Нужных нет — на машине с Ollama: <code>ollama pull qwen2.5:7b</code>
|
||||
и <code>ollama pull nomic-embed-text</code>.</p>
|
||||
|
||||
<div class="mt"><button class="act" id="cfgSave">Сохранить</button></div>
|
||||
<p class="hint" id="cfgWhere"></p>
|
||||
</section>
|
||||
|
||||
<!-- ================= Обзор ================= -->
|
||||
<section class="tab" id="tab-tree">
|
||||
<button class="act ghost" id="treeBtn">Обновить</button>
|
||||
@@ -254,21 +285,93 @@ const LAST = {
|
||||
function say(text) { $('#reply').style.display = 'block'; $('#reply').textContent = text || ''; }
|
||||
|
||||
// ---------- вкладки ----------
|
||||
document.querySelectorAll('nav button').forEach(b => b.onclick = () => {
|
||||
document.querySelectorAll('nav button').forEach(x => x.classList.toggle('on', x === b));
|
||||
document.querySelectorAll('.tab').forEach(t => t.classList.toggle('on', t.id === 'tab-' + b.dataset.tab));
|
||||
if (b.dataset.tab === 'tree') loadTree();
|
||||
});
|
||||
function openTab(name) {
|
||||
document.querySelectorAll('nav button').forEach(x =>
|
||||
x.classList.toggle('on', x.dataset.tab === name));
|
||||
document.querySelectorAll('.tab').forEach(t =>
|
||||
t.classList.toggle('on', t.id === 'tab-' + name));
|
||||
if (name === 'tree') loadTree();
|
||||
if (name === 'setup') loadConfig();
|
||||
window.scrollTo({top: 0});
|
||||
}
|
||||
document.querySelectorAll('nav button').forEach(b => b.onclick = () => openTab(b.dataset.tab));
|
||||
|
||||
// ---------- баннер ----------
|
||||
api('/api/health').then(h => {
|
||||
if (!h.ollama) {
|
||||
$('#banner').style.display = 'block';
|
||||
$('#banner').textContent = 'ИИ недоступен (' + h.host + '). Кнопки работают полностью; '
|
||||
+ 'не будет только синонимов и разбора свободных фраз.';
|
||||
}
|
||||
if (h.ollama) return;
|
||||
const b = $('#banner');
|
||||
b.style.display = 'block';
|
||||
b.textContent = h.fresh
|
||||
? 'Похоже, помощник только установлен, а Ollama по адресу ' + h.host + ' не отвечает. '
|
||||
: 'ИИ недоступен (' + h.host + '). Кнопки работают полностью; не будет только '
|
||||
+ 'тегов и разбора свободных фраз. ';
|
||||
const link = document.createElement('a');
|
||||
link.href = '#';
|
||||
link.textContent = 'Настроить →';
|
||||
link.style.color = 'inherit';
|
||||
link.style.fontWeight = '700';
|
||||
link.onclick = e => { e.preventDefault(); openTab('setup'); };
|
||||
b.appendChild(link);
|
||||
}).catch(() => {});
|
||||
|
||||
// ---------- настройки ----------
|
||||
async function loadConfig() {
|
||||
const d = await api('/api/config');
|
||||
$('#cfgHost').value = d.ollama_host || '';
|
||||
fillModels(d.models || [], d.chat_model, d.embed_model);
|
||||
$('#cfgWhere').textContent = 'Настройки хранятся в файле ' + d.env_file
|
||||
+ ' — он не попадает в репозиторий.';
|
||||
if (!d.available) {
|
||||
$('#cfgProbeResult').innerHTML =
|
||||
'<div class="card"><div class="addr taken">Ollama по этому адресу не отвечает.</div></div>';
|
||||
}
|
||||
}
|
||||
|
||||
// Модель выбирается из установленных; текущее значение показываем даже если
|
||||
// его нет в списке — иначе выбор молча сбросился бы на чужую модель.
|
||||
function fillModels(models, chat, embed) {
|
||||
const opts = extra => ['<option value="">— не задана —</option>']
|
||||
.concat((models.length ? models : []).map(m =>
|
||||
'<option value="' + esc(m) + '">' + esc(m) + '</option>'))
|
||||
.concat(extra && !models.includes(extra)
|
||||
? ['<option value="' + esc(extra) + '">' + esc(extra) + ' (не установлена)</option>'] : [])
|
||||
.join('');
|
||||
$('#cfgChat').innerHTML = opts(chat);
|
||||
$('#cfgEmbed').innerHTML = opts(embed);
|
||||
$('#cfgChat').value = chat || '';
|
||||
$('#cfgEmbed').value = embed || '';
|
||||
}
|
||||
|
||||
$('#cfgProbe').onclick = async () => {
|
||||
const btn = $('#cfgProbe');
|
||||
btn.disabled = true; btn.textContent = 'Проверяю…';
|
||||
try {
|
||||
const d = await api('/api/config/probe', {method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ollama_host: $('#cfgHost').value.trim()})});
|
||||
$('#cfgProbeResult').innerHTML = '<div class="card"><div class="addr'
|
||||
+ (d.ok ? '' : ' taken') + '">' + esc(d.reply || '') + '</div></div>';
|
||||
if (d.ok) fillModels(d.models || [], $('#cfgChat').value, $('#cfgEmbed').value);
|
||||
} finally { btn.disabled = false; btn.textContent = 'Проверить связь'; }
|
||||
};
|
||||
|
||||
$('#cfgSave').onclick = async () => {
|
||||
const btn = $('#cfgSave');
|
||||
btn.disabled = true; btn.textContent = 'Сохраняю…';
|
||||
try {
|
||||
const d = await api('/api/config', {method:'POST',
|
||||
headers:{'Content-Type':'application/json'},
|
||||
body: JSON.stringify({ollama_host: $('#cfgHost').value.trim(),
|
||||
chat_model: $('#cfgChat').value,
|
||||
embed_model: $('#cfgEmbed').value})});
|
||||
say(d.reply);
|
||||
if (d.ok) {
|
||||
$('#banner').style.display = d.available ? 'none' : 'block';
|
||||
loadConfig();
|
||||
}
|
||||
} finally { btn.disabled = false; btn.textContent = 'Сохранить'; }
|
||||
};
|
||||
|
||||
// ---------- режимы ----------
|
||||
const LABEL = {put:'Положить', take:'Забрать', move:'Переместить', delete:'Удалить'};
|
||||
document.querySelectorAll('.modes button').forEach(b => b.onclick = () => {
|
||||
|
||||
Reference in New Issue
Block a user