feat: the assistant's answers in Markdown; thinking shows «Думаю…», then «Думал N с», opening smoothly by its arrow
This commit is contained in:
1 parent
4b7220fffb
commit
4da1cc4200
4 files changed
+103
-12
No files matched your search
@@ -298,20 +298,35 @@
|
||||
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.
|
||||
// render draws an answer: its thinking, folded, above; the text with
|
||||
// Markdown; ``` blocks apart, action blocks with their button. The
|
||||
// thinking block is made once and only its words change, so it stays open
|
||||
// or shut as the user left it while the answer streams.
|
||||
const CHEV = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><path d="m9 6 6 6-6 6"/></svg>';
|
||||
function render(el, text, done) {
|
||||
// Thinking first, folded: ::думаю:: … ::ответ:: (or <think>), open while
|
||||
// it is going on.
|
||||
let html = '', n = 0;
|
||||
let think = el.querySelector(':scope > .ai-think'), body = el.querySelector(':scope > .ai-body');
|
||||
if (!body) {
|
||||
el.innerHTML = '<div class="ai-think" hidden><button type="button" class="ai-think-h">' + CHEV + '<span></span></button>' +
|
||||
'<div class="ai-think-b"><div class="ai-think-in"></div></div></div><div class="ai-body"></div>';
|
||||
think = el.querySelector('.ai-think'); body = el.querySelector('.ai-body');
|
||||
think.querySelector('.ai-think-h').onclick = () => think.classList.toggle('open');
|
||||
}
|
||||
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>';
|
||||
const going = !th[2] && !done;
|
||||
if (!el._t0) el._t0 = Date.now();
|
||||
if (!going && !el._t1) el._t1 = Date.now();
|
||||
think.hidden = false;
|
||||
think.classList.toggle('going', going);
|
||||
think.querySelector('.ai-think-h span').textContent = going ? 'Думаю…'
|
||||
: 'Думал ' + Math.max(1, Math.round((el._t1 - el._t0) / 1000)) + ' с';
|
||||
think.querySelector('.ai-think-in').innerHTML = WSUtil.md(th[1].trim());
|
||||
text = text.slice(th[0].length);
|
||||
}
|
||||
let html = '', n = 0;
|
||||
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; }
|
||||
if (i % 2 === 0) { if (part.trim()) html += '<div class="ai-text">' + WSUtil.md(part) + '</div>'; return; }
|
||||
const m = /^([^\n]*)\n?([\s\S]*)$/.exec(part);
|
||||
const head = m[1].trim(), kind = head.split(/\s+/)[0], code = m[2].replace(/\n$/, '');
|
||||
const closed = i < parts.length - 1 || done;
|
||||
@@ -320,7 +335,7 @@
|
||||
(code || !act ? '<pre>' + esc(code) + '</pre>' : '') +
|
||||
(act ? '<button type="button" class="btn small ai-act" data-i="' + (n++) + '">' + LABEL[kind] + '</button>' : '') + '</div>';
|
||||
});
|
||||
el.innerHTML = html;
|
||||
body.innerHTML = html;
|
||||
el.dataset.text = text;
|
||||
}
|
||||
function add(role, text) {
|
||||
|
||||
Reference in new issue
Block a user