feat: the assistant's Агент and Думать modes as pills; thinking shows folded above the answer
This commit is contained in:
1 parent
847bcd68c0
commit
8d7489b577
3 files changed
+51
-7
No files matched your search
@@ -20,10 +20,16 @@
|
||||
box.className = 'ai-box';
|
||||
box.innerHTML =
|
||||
'<div class="ai-head"><b>Ассистент</b><span class="muted ai-where"></span><span class="spacer"></span>' +
|
||||
'<label class="ai-agent" title="Агент: сам выполняет шаги один за другим, спрашивая перед каждой командой и записью файла"><input type="checkbox"> Агент</label>' +
|
||||
|
||||
'<button type="button" class="btn ghost small ai-clear" title="Новый разговор">Очистить</button>' +
|
||||
'<button type="button" class="btn ghost small icon close x ai-close" aria-label="Закрыть"><svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.6" stroke-linecap="round"><path d="M18 6 6 18M6 6l12 12"/></svg></button></div>' +
|
||||
'<div class="ai-list"><p class="muted ai-hello">Спросите о том, что открыто в панели: ассистент видит вкладки и активный терминал или папку. Команды он предложит кнопкой «Выполнить», умеет читать и писать файлы на любой вашей машине. В режиме «Агент» он делает шаги сам, спрашивая перед каждой командой.</p></div>' +
|
||||
'<div class="ai-pills">' +
|
||||
'<button type="button" class="ai-pill" data-mode="agent" aria-pressed="false" title="Сам выполняет шаги один за другим, спрашивая перед каждой командой и записью файла">' +
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 8V4H8"/><rect x="4" y="8" width="16" height="12" rx="2"/><path d="M2 14h2M20 14h2M9 13v2M15 13v2"/></svg>Агент</button>' +
|
||||
'<button type="button" class="ai-pill" data-mode="think" aria-pressed="false" title="Сначала обдумывает, потом отвечает: дольше, но точнее">' +
|
||||
'<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 18h6M10 22h4"/><path d="M12 2a7 7 0 0 0-4 12.7c.6.5 1 1.3 1 2.3h6c0-1 .4-1.8 1-2.3A7 7 0 0 0 12 2z"/></svg>Думать</button>' +
|
||||
'</div>' +
|
||||
'<form class="ai-form-in"><textarea rows="1" placeholder="Сообщение…"></textarea>' +
|
||||
'<button type="submit" class="ai-send" title="Отправить" aria-label="Отправить">' + PLANE + '</button></form>';
|
||||
document.body.append(fab, box);
|
||||
@@ -193,11 +199,22 @@
|
||||
}
|
||||
|
||||
// ---------- messages ----------
|
||||
// stripThink: the answer without its thinking - what is acted on and kept.
|
||||
const stripThink = t => t.replace(/^\s*(?:<think>|::думаю::)[\s\S]*?((?:<\/think>|::ответ::)\s*|$)/, '');
|
||||
const LABEL = {run: 'Выполнить', read: 'Прочитать', ls: 'Показать', write: 'Записать'};
|
||||
// render: text with ``` blocks; action blocks get their button.
|
||||
function render(el, text, done) {
|
||||
const parts = text.split(/```/);
|
||||
// Thinking first, folded: ::думаю:: … ::ответ:: (or <think>), open while
|
||||
// it is going on.
|
||||
let html = '', n = 0;
|
||||
const th = /^\s*(?:<think>|::думаю::)([\s\S]*?)(<\/think>|::ответ::|$)/.exec(text);
|
||||
if (th) {
|
||||
const going = !th[2];
|
||||
html += '<details class="ai-think"' + (going ? ' open' : '') + '><summary>' + (going ? 'Думает…' : 'Размышления') + '</summary><div>' +
|
||||
esc(th[1].trim()).replace(/\n/g, '<br>') + '</div></details>';
|
||||
text = text.slice(th[0].length);
|
||||
}
|
||||
const parts = text.split(/```/);
|
||||
parts.forEach((part, i) => {
|
||||
if (i % 2 === 0) { html += '<div class="ai-text">' + esc(part).replace(/\n/g, '<br>') + '</div>'; return; }
|
||||
const m = /^([^\n]*)\n?([\s\S]*)$/.exec(part);
|
||||
@@ -250,7 +267,7 @@
|
||||
await loadMachines();
|
||||
const r = await fetch('/api/panel/ai/chat', {
|
||||
method: 'POST', headers: {'Content-Type': 'application/json'}, credentials: 'same-origin', signal: ac.signal,
|
||||
body: JSON.stringify({messages: talk, context: context(), agent: agentOn()})
|
||||
body: JSON.stringify({messages: talk, context: context(), agent: agentOn(), think: thinkOn()})
|
||||
});
|
||||
if (r.redirected && new URL(r.url).pathname === '/login') { location.href = '/login'; return; }
|
||||
if (!r.body) throw new Error('ошибка ' + r.status);
|
||||
@@ -268,11 +285,22 @@
|
||||
} finally {
|
||||
el.classList.remove('wait');
|
||||
render(el, answer || '[нет ответа]', true);
|
||||
talk.push({role: 'assistant', content: answer});
|
||||
talk.push({role: 'assistant', content: stripThink(answer)});
|
||||
}
|
||||
return {el, answer};
|
||||
return {el, answer: stripThink(answer)};
|
||||
}
|
||||
const agentOn = () => q('.ai-agent input').checked;
|
||||
const pill = k => q('.ai-pill[data-mode="' + k + '"]');
|
||||
const agentOn = () => pill('agent').getAttribute('aria-pressed') === 'true';
|
||||
const thinkOn = () => pill('think').getAttribute('aria-pressed') === 'true';
|
||||
// The modes are kept in this browser.
|
||||
box.querySelectorAll('.ai-pill').forEach(b => {
|
||||
try { b.setAttribute('aria-pressed', String(localStorage.getItem('lpra.ai.' + b.dataset.mode) === '1')); } catch (e) { /* off */ }
|
||||
b.onclick = () => {
|
||||
const on = b.getAttribute('aria-pressed') !== 'true';
|
||||
b.setAttribute('aria-pressed', String(on));
|
||||
try { localStorage.setItem('lpra.ai.' + b.dataset.mode, on ? '1' : ''); } catch (e) { /* not kept */ }
|
||||
};
|
||||
});
|
||||
// talkTurn: one question, and in agent mode the steps after it - the
|
||||
// first action of each answer done (asking first for commands and writes)
|
||||
// and its result sent back - until an answer asks for nothing.
|
||||
|
||||
Reference in new issue
Block a user