feat: the assistant's chat - the name clear of the close button, typing dots while the model starts, thinking that opens without reflow; asking before a command: «Выполнить команду на [машина]?», the command in mono, a red «Отмена»
This commit is contained in:
1 parent
3641a3627c
commit
fcf9417f5a
3 files changed
+55
-10
No files matched your search
@@ -356,10 +356,16 @@
|
||||
}
|
||||
// ask first for what changes something: commands and writes.
|
||||
function needsYes(a) { return a.kind === 'run' || a.kind === 'write'; }
|
||||
function describe(a) {
|
||||
const where = a.machine ? a.machine + ': ' : '';
|
||||
return a.kind === 'run' ? 'Выполнить на ' + (a.machine || 'активной машине') + ':\n\n' + a.body
|
||||
: 'Записать файл ' + where + a.path + ' (' + a.body.length + ' символов)?';
|
||||
// ask what a command or a write is about to do: what, where, the command
|
||||
// (or the start of the text) in mono.
|
||||
function confirmAction(a) {
|
||||
const m = machineOf(a.machine), where = m ? m.name : (a.machine || 'активная машина');
|
||||
if (a.kind === 'run') return UI.confirm('Выполнить команду на {chip}?', {ok: 'Выполнить', chip: where, code: a.body,
|
||||
note: 'Уйдёт в терминал этой машины с вашими правами.'});
|
||||
const lines = a.body.split('\n');
|
||||
return UI.confirm('Записать файл {chip}?', {ok: 'Записать', chip: where + ':' + a.path,
|
||||
code: lines.slice(0, 12).join('\n') + (lines.length > 12 ? '\n…' : ''),
|
||||
note: 'Файл будет заменён целиком (' + a.body.length + ' символов).'});
|
||||
}
|
||||
|
||||
// ---------- messages ----------
|
||||
@@ -463,6 +469,8 @@
|
||||
if (res) showResult(res);
|
||||
const el = add('assistant', '');
|
||||
el.classList.add('wait');
|
||||
// While the model starts: a typing mark, gone with the first words.
|
||||
el.querySelector('.ai-body').innerHTML = '<span class="ai-typing" aria-label="печатает"><i></i><i></i><i></i></span>';
|
||||
ac = new AbortController();
|
||||
send.innerHTML = STOP; send.title = 'Остановить';
|
||||
let answer = '';
|
||||
@@ -521,7 +529,7 @@
|
||||
// Without «Агент» only searches go on by themselves, with «Поиск» on.
|
||||
if (!agentOn() && !(a.kind === 'search' && searchOn())) break;
|
||||
const b = turn.el.querySelector('.ai-act');
|
||||
if (needsYes(a) && !await UI.confirm(describe(a), {ok: a.kind === 'run' ? 'Выполнить' : 'Записать'})) {
|
||||
if (needsYes(a) && !await confirmAction(a)) {
|
||||
if (b) { b.disabled = true; b.textContent = 'Отклонено'; }
|
||||
turn = await ask(null, 'Пользователь отклонил это действие. Предложи другое или закончи.');
|
||||
continue;
|
||||
|
||||
@@ -856,7 +856,11 @@ body.touch.term-active .ai-fab { bottom:104px; }
|
||||
.ai-msg { max-width:92%; padding:8px 11px; border-radius:10px; font-size:14px; line-height:1.45; overflow-wrap:anywhere; }
|
||||
.ai-msg.user { align-self:flex-end; background:#16324f; white-space:pre-wrap; }
|
||||
.ai-msg.assistant { align-self:flex-start; background:#151a21; border:1px solid var(--border); }
|
||||
.ai-msg.wait::after { content:'…'; color:var(--muted); }
|
||||
/* The model starting: three dots rising in turn. */
|
||||
.ai-typing { display:inline-flex; gap:4px; padding:4px 2px; }
|
||||
.ai-typing i { width:6px; height:6px; border-radius:50%; background:var(--muted); animation:ai-typing 1.2s ease-in-out infinite; }
|
||||
.ai-typing i:nth-child(2) { animation-delay:.15s; } .ai-typing i:nth-child(3) { animation-delay:.3s; }
|
||||
@keyframes ai-typing { 0%, 60%, 100% { transform:translateY(0); opacity:.4; } 30% { transform:translateY(-5px); opacity:1; } }
|
||||
.ai-code { margin:6px 0; }
|
||||
.ai-code pre { margin:0; padding:8px; background:#0c0f13; border:1px solid var(--border); border-radius:7px; overflow:auto; font-size:12.5px; white-space:pre-wrap; }
|
||||
.ai-code .ai-run { margin-top:6px; }
|
||||
@@ -950,8 +954,12 @@ main#app > .view:not([hidden]) { animation:view-in .22s ease; } /* no fill: a la
|
||||
@keyframes ai-shine { to { background-position:-200% 0; } }
|
||||
.ai-think-b { display:grid; grid-template-rows:0fr; transition:grid-template-rows .22s ease, opacity .2s ease; opacity:0; }
|
||||
.ai-think.open .ai-think-b { grid-template-rows:1fr; opacity:1; }
|
||||
.ai-think-in { min-height:0; overflow:hidden; font-size:12px; color:var(--muted); line-height:1.45; }
|
||||
.ai-think.open .ai-think-in { margin-top:4px; padding-left:10px; border-left:2px solid var(--border); max-height:220px; overflow:auto; }
|
||||
.ai-think-in { min-height:0; overflow:hidden; font-size:12px; color:var(--muted); line-height:1.45;
|
||||
margin-top:4px; padding-left:10px; border-left:2px solid var(--border); }
|
||||
.ai-think.open .ai-think-in { max-height:220px; overflow:auto; }
|
||||
/* With thinking, the answer keeps its width: the thinking opens without
|
||||
reflowing (short answers made the bubble narrow, the words then moved). */
|
||||
.ai-msg.assistant:has(> .ai-think:not([hidden])) { width:92%; }
|
||||
.ai-think-in p { margin:0 0 4px; }
|
||||
/* Folding parts (Сменить пароль, PIN-код, more settings, results) open and
|
||||
shut smoothly, their arrow turning, as the + menu's groups do. */
|
||||
@@ -966,7 +974,7 @@ details::details-content { block-size: 0; overflow: hidden; opacity: 0;
|
||||
details[open]::details-content { block-size: auto; opacity: 1; }
|
||||
/* The assistant's chats: the name in the header, the list under it. */
|
||||
.ai-head { position:relative; }
|
||||
.ai-chatsel { display:inline-flex; align-items:center; gap:6px; min-width:0; max-width:65%; padding:4px 6px; margin-left:-6px; border-radius:7px;
|
||||
.ai-chatsel { display:inline-flex; align-items:center; gap:6px; min-width:0; max-width:calc(100% - 64px); padding:4px 6px; margin-left:-6px; border-radius:7px;
|
||||
background:none; border:0; color:var(--text); font-weight:600; font-size:14px; cursor:pointer; }
|
||||
.ai-chatsel:hover { background:#151a21; }
|
||||
.ai-chatname { overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
@@ -1029,3 +1037,11 @@ body.no-motion *, body.no-motion *::before, body.no-motion *::after, body.no-mot
|
||||
.ai-form .ai-sub { margin:10px 0 0; font-size:15px; }
|
||||
.ai-form:not([data-search="searxng"]) .s-url { display:none; }
|
||||
.ai-form:not([data-search="brave"]):not([data-search="tavily"]) .s-key { display:none; }
|
||||
/* A question about an action: where, the command in mono, a note. */
|
||||
.ui-extra { display:grid; gap:8px; margin-top:-4px; min-width:0; }
|
||||
.ui-chip { justify-self:start; display:inline-block; vertical-align:middle; margin:-2px 2px 0; max-width:100%; padding:1px 8px; line-height:18px; border-radius:999px; background:#12202f; color:#9db8ff;
|
||||
font:12px ui-monospace, SFMono-Regular, Menlo, monospace; overflow:hidden; text-overflow:ellipsis; white-space:nowrap; }
|
||||
.ui-code { margin:0; padding:9px 11px; background:#0c0f13; border:1px solid var(--border); border-radius:8px; max-height:220px; overflow:auto;
|
||||
font:12.5px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace; white-space:pre; color:var(--text); }
|
||||
/* A command is shown as it is: no line broken inside it, a long one scrolls. */
|
||||
.ui-note { margin:0; font-size:12px; color:var(--muted); }
|
||||
@@ -9,7 +9,7 @@
|
||||
dlg = document.createElement('dialog');
|
||||
dlg.className = 'uidlg';
|
||||
dlg.innerHTML = '<form method="dialog"><p class="ui-text"></p><input class="ui-input" autocomplete="off" spellcheck="false">' +
|
||||
'<div class="row end"><button type="button" class="btn ghost ui-no">Отмена</button><button type="submit" class="ui-yes">OK</button></div></form>';
|
||||
'<div class="row end"><button type="button" class="btn ghost close ui-no">Отмена</button><button type="submit" class="ui-yes">OK</button></div></form>';
|
||||
document.body.appendChild(dlg);
|
||||
}
|
||||
function ask(text, o) {
|
||||
@@ -20,6 +20,9 @@
|
||||
// is as wide as its words, up to the screen's room.
|
||||
const t = dlg.querySelector('.ui-text');
|
||||
t.textContent = '';
|
||||
// "{chip}" in the text: the chip stands there, inside the sentence.
|
||||
const parts = String(text).split('{chip}');
|
||||
text = parts.shift();
|
||||
String(text).split(/(\s+)/).forEach(w => {
|
||||
if (w.length > 28 && !/\s/.test(w)) {
|
||||
const s = document.createElement('span');
|
||||
@@ -27,10 +30,28 @@
|
||||
t.appendChild(s);
|
||||
} else t.appendChild(document.createTextNode(w));
|
||||
});
|
||||
// Extras of a question about an action: where (a chip), the command in
|
||||
// mono, a note under it.
|
||||
if (parts.length && o.chip) {
|
||||
const c = document.createElement('span'); c.className = 'ui-chip'; c.textContent = o.chip; c.title = o.chip;
|
||||
t.append(c, parts.join(''));
|
||||
o = Object.assign({}, o, {chip: ''});
|
||||
}
|
||||
dlg.querySelectorAll('.ui-extra').forEach(e => e.remove());
|
||||
if (o.chip || o.code || o.note) {
|
||||
const box = document.createElement('div');
|
||||
box.className = 'ui-extra';
|
||||
if (o.chip) { const c = document.createElement('div'); c.className = 'ui-chip'; c.textContent = o.chip; c.title = o.chip; box.appendChild(c); }
|
||||
if (o.code) { const p = document.createElement('pre'); p.className = 'ui-code'; p.textContent = o.code; box.appendChild(p); }
|
||||
if (o.note) { const n = document.createElement('p'); n.className = 'ui-note'; n.textContent = o.note; box.appendChild(n); }
|
||||
t.after(box);
|
||||
}
|
||||
input.hidden = !o.input;
|
||||
input.value = o.value || '';
|
||||
yes.textContent = o.ok || 'OK';
|
||||
yes.className = 'ui-yes' + (o.danger ? ' btn danger' : '');
|
||||
// «Отмена» red, unless «да» is the red one (a delete): never two reds.
|
||||
dlg.querySelector('.ui-no').classList.toggle('close', !o.danger);
|
||||
return new Promise(done => {
|
||||
const end = v => { dlg.onclose = null; done(v); };
|
||||
dlg.querySelector('.ui-no').onclick = () => dlg.close('');
|
||||
|
||||
Reference in new issue
Block a user