Files
agentik-desktop/approved/004-settings-agents.html
T

405 lines
20 KiB
HTML
Raw 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.
<!doctype html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>Одобрено: Агенты — список, форма, проверка связи</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--bg: #121218; --panel: #17212B; --panel2: #1C2733; --line: #232B36;
--fg: #EBEBEB; --dim: #768C9E; --accent: #6AB2F2;
--err: #EC3942; --ok: #63C88A;
}
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
-webkit-font-smoothing: antialiased;
background: #0A0A0F; color: var(--fg); line-height: 1.45;
display: flex; align-items: center; justify-content: center; min-height: 100vh; padding: 24px;
position: relative;
}
/* ============ затемнение: чуть гасит окно под окном проверки ============ */
.scrim { position: fixed; inset: 0; background: rgba(0,0,0,.42); z-index: 200; display: none; }
.scrim.on { display: block; }
/* ============ ОКНО 1: НАСТРОЙКИ ============ */
.dialog {
width: 620px; min-width: 620px; flex: 0 0 auto; max-height: 620px; background: var(--panel);
border: 1px solid var(--line); border-radius: 14px; display: flex; flex-direction: column;
overflow: hidden; box-shadow: 0 26px 80px rgba(0,0,0,.7); position: relative; z-index: 150;
}
.d-head { display: flex; align-items: center; gap: 11px; padding: 16px 18px 14px; border-bottom: 1px solid var(--line); }
.d-head h2 { font-size: 16px; font-weight: 600; flex: 1; }
.x-btn { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 17px; }
.x-btn:hover { background: var(--panel2); color: var(--fg); }
.d-body { flex: 1; overflow-y: auto; padding: 14px 18px 6px; }
.d-foot { padding: 13px 18px 16px; border-top: 1px solid var(--line); display: flex; gap: 9px; }
/* ── список агентов ── */
.agent-card {
display: flex; align-items: center; gap: 12px; padding: 11px 12px; border-radius: 11px;
background: var(--panel2); margin-bottom: 9px; border: 1px solid transparent;
}
.agent-card:hover { border-color: var(--line); }
.avatar {
width: 40px; height: 40px; flex: 0 0 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 13px; font-weight: 600; color: #0F1922; background: var(--accent); overflow: hidden;
}
.avatar.g2 { background: #86C99A; }
.avatar.g3 { background: #C9A886; }
/* значок-картинка: клиент показывает свою копию файла */
.avatar.pic { background: transparent; padding: 0; }
.avatar.pic img { width: 100%; height: 100%; object-fit: cover; display: block; }
.ag-mid { flex: 1; min-width: 0; }
.ag-name { font-size: 14px; font-weight: 550; }
.ag-addr { font-size: 11.5px; color: var(--dim); font-family: ui-monospace, "SF Mono", Menlo, monospace;
white-space: nowrap; overflow: hidden; text-overflow: ellipsis; margin-top: 3px; }
.state { display: flex; align-items: center; gap: 5px; font-size: 11px; color: var(--ok); flex: 0 0 auto; }
.state .d { width: 7px; height: 7px; border-radius: 50%; background: var(--ok); }
.state.off { color: var(--dim); } .state.off .d { background: var(--dim); }
.state.err { color: var(--err); } .state.err .d { background: var(--err); }
.card-acts { display: flex; gap: 4px; flex: 0 0 auto; }
.mini-ic { width: 30px; height: 30px; border-radius: 8px; border: none; background: transparent;
color: var(--dim); cursor: pointer; font-size: 14px; }
.mini-ic:hover { background: var(--panel); color: var(--fg); }
.mini-ic.del:hover { color: var(--err); }
/* ── форма ── */
.field { margin-bottom: 15px; }
.field label { display: block; font-size: 12px; color: var(--dim); margin-bottom: 6px; }
.field input {
width: 100%; padding: 10px 12px; border-radius: 10px; border: 1px solid var(--line);
background: var(--bg); color: var(--fg); font-size: 13.5px; outline: none; font-family: inherit;
}
.field input:focus { border-color: var(--accent); }
.field input.mono { font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 12.5px; }
.field .note { font-size: 11px; color: var(--dim); margin-top: 6px; }
.note code { background: var(--panel2); padding: 1px 4px; border-radius: 4px; font-size: 10.5px; }
.addr-row { display: flex; gap: 8px; }
.addr-row input { flex: 1; }
/* ── цвет ИЛИ картинка ── */
.appearance-tabs { display: flex; gap: 6px; margin-bottom: 11px; }
.appearance-tabs .tab {
padding: 6px 13px; border-radius: 9px; font-size: 12.5px; color: var(--dim);
background: var(--panel2); cursor: pointer; border: 1px solid transparent;
}
.appearance-tabs .tab.on { color: var(--fg); border-color: var(--accent); background: rgba(106,178,242,.12); }
.pane { display: none; } .pane.on { display: block; }
.colors { display: flex; gap: 9px; }
.swatch { width: 30px; height: 30px; border-radius: 50%; cursor: pointer; border: 2px solid transparent; }
.swatch.on { border-color: var(--fg); }
.pic-row { display: flex; align-items: center; gap: 13px; }
.pic-preview {
width: 54px; height: 54px; border-radius: 50%; flex: 0 0 54px; background: var(--bg);
border: 1px dashed var(--line); display: grid; place-items: center; color: var(--dim);
font-size: 20px; overflow: hidden;
}
.pic-preview.filled { border-style: solid; }
.pic-preview img { width: 100%; height: 100%; object-fit: cover; display: block; }
.pic-actions { display: flex; gap: 8px; flex-wrap: wrap; }
/* ── кнопки ── */
.btn { border-radius: 10px; padding: 10px 17px; font-size: 13px; cursor: pointer; border: 1px solid var(--line);
background: transparent; color: var(--fg); font-family: inherit; white-space: nowrap; }
.btn:hover { border-color: var(--dim); }
.btn.prim { background: var(--accent); border-color: var(--accent); color: #0F1922; font-weight: 650; }
.btn.prim:disabled { background: var(--panel2); border-color: var(--line); color: var(--dim); cursor: default; }
.btn.sm { padding: 8px 13px; font-size: 12.5px; }
.btn.wide { width: 100%; display: flex; align-items: center; justify-content: center; gap: 8px;
border-style: dashed; color: var(--dim); }
.btn.wide:hover { color: var(--fg); border-color: var(--dim); }
/* ============ ОКНО 2: ПРОВЕРКА СВЯЗИ (поверх настроек) ============ */
.layer { position: fixed; inset: 0; display: none; align-items: center; justify-content: center;
z-index: 250; pointer-events: none; }
.layer.on { display: flex; }
.layer > * { pointer-events: auto; }
.dialog.small { width: 480px; min-width: 480px; max-height: 420px; }
.spin { width: 17px; height: 17px; border-radius: 50%; border: 2px solid var(--line);
border-top-color: var(--accent); animation: sp .7s linear infinite; flex: 0 0 17px; }
@keyframes sp { to { transform: rotate(360deg); } }
.probe-head { display: flex; align-items: center; gap: 13px; padding: 4px 0 14px; }
.probe-head .big { width: 40px; height: 40px; border-radius: 50%; display: grid; place-items: center;
font-size: 19px; flex: 0 0 40px; }
.probe-head.ok .big { background: rgba(99,200,138,.15); color: var(--ok); }
.probe-head.err .big { background: rgba(236,57,66,.15); color: var(--err); }
.probe-head.busy .big { background: transparent; padding: 0; }
.probe-head .txt { flex: 1; }
.probe-head .ttl { font-size: 14px; font-weight: 600; }
.probe-head .sub { font-size: 12px; color: var(--dim); margin-top: 2px; }
.probe-list { font-size: 12px; }
.probe-list div { display: flex; justify-content: space-between; gap: 14px; padding: 7px 0;
border-bottom: 1px solid var(--line); }
.probe-list div:last-child { border-bottom: none; }
.probe-list span { color: var(--dim); }
.probe-list b { font-weight: 550; font-family: ui-monospace, "SF Mono", Menlo, monospace; font-size: 11.5px; }
.hint-line { font-size: 11.5px; color: var(--dim); margin-top: 12px; padding: 9px 11px;
background: var(--bg); border-radius: 9px; border: 1px solid var(--line); }
</style>
</head>
<body>
<!-- ================= ОКНО 1: НАСТРОЙКИ ================= -->
<div class="dialog">
<div class="d-head">
<h2 id="dTitle">Агенты</h2>
<button class="x-btn">✕</button>
</div>
<!-- ── вид 1: список ── -->
<div class="d-body" id="viewList">
<div class="agent-card">
<div class="avatar">АГ</div>
<div class="ag-mid">
<div class="ag-name">agentik · рабочий</div>
<div class="ag-addr">http://192.168.88.135:8080/agentik</div>
</div>
<div class="state"><span class="d"></span>на связи</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.88.135:8080/agentik',true)">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="agent-card">
<div class="avatar g2">ДМ</div>
<div class="ag-mid">
<div class="ag-name">домашний</div>
<div class="ag-addr">http://192.168.76.199:8080/agentik</div>
</div>
<div class="state err"><span class="d"></span>не отвечает</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.76.199:8080/agentik',false)">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<div class="agent-card">
<div class="avatar pic"><img src="data:image/svg+xml;utf8,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 40 40'%3E%3Crect width='40' height='40' fill='%232F4A63'/%3E%3Ccircle cx='20' cy='15' r='6.5' fill='%23DCE6F0'/%3E%3Cpath d='M7 40c0-7.5 5.8-12 13-12s13 4.5 13 12z' fill='%23DCE6F0'/%3E%3C/svg%3E" alt=""></div>
<div class="ag-mid">
<div class="ag-name">Кот-ассистент</div>
<div class="ag-addr">http://192.168.88.24:8080/agentik</div>
</div>
<div class="state off"><span class="d"></span>не проверялся</div>
<div class="card-acts">
<button class="mini-ic" title="Проверить связь" onclick="openCheck('list','http://192.168.88.24:8080/agentik',true)">⇄</button>
<button class="mini-ic" title="Изменить">✎</button>
<button class="mini-ic del" title="Удалить">🗑</button>
</div>
</div>
<button class="btn wide" onclick="show('add')">+ Добавить агента</button>
</div>
<!-- ── вид 2: форма нового ── -->
<div class="d-body" id="viewAdd" style="display:none">
<div class="field">
<label>Название</label>
<input type="text" value="рабочий" placeholder="как вы его будете звать">
<div class="note">Как называть этого агента в списке. Придумываете сами — сервер своего имени не сообщает.</div>
</div>
<div class="field">
<label>Адрес сервера агента</label>
<div class="addr-row">
<input type="text" class="mono" id="addrField" value="http://192.168.88.135:8080/agentik"
placeholder="http://хост:порт/agentik">
<button class="btn sm" onclick="openCheck('form', document.getElementById('addrField').value, true)">Проверить связь</button>
</div>
<div class="note">Адрес целиком, вместе с путём <code>/agentik</code>. Если агент
на этой же машине — <code>http://localhost:8080/agentik</code>.</div>
</div>
<div class="field">
<label>Как отличать в списке</label>
<div class="appearance-tabs">
<div class="tab on" data-tab="color">Цвет</div>
<div class="tab" data-tab="pic">Картинка</div>
</div>
<div class="pane on" id="paneColor">
<div class="colors">
<div class="swatch on" style="background:#6AB2F2"></div>
<div class="swatch" style="background:#86C99A"></div>
<div class="swatch" style="background:#C9A886"></div>
<div class="swatch" style="background:#B79AE0"></div>
<div class="swatch" style="background:#E28C8C"></div>
</div>
<div class="note">Цветом помечаются диалоги агента в списке — полоска у строки
и точка у группы.</div>
</div>
<div class="pane" id="panePic">
<div class="pic-row">
<div class="pic-preview" id="picPreview">🖼</div>
<div class="pic-actions">
<button class="btn sm" onclick="pickPic()">Выбрать файл…</button>
<button class="btn sm" id="picClear" onclick="clearPic()" style="display:none">Убрать</button>
</div>
</div>
<div class="note">Картинка копируется <b>внутрь клиента</b> — по исходному адресу
не подтягивается. Иначе значок пропадёт, если файл удалят, флешку вынут
или папку переименуют.</div>
</div>
</div>
</div>
<div class="d-foot" id="footList">
<button class="btn prim" style="flex:1">Готово</button>
</div>
<div class="d-foot" id="footAdd" style="display:none">
<button class="btn" style="flex:1" onclick="show('list')">Отмена</button>
<button class="btn prim" style="flex:1">Добавить агента</button>
</div>
</div>
<div class="scrim" id="scrim"></div>
<!-- ================= ОКНО 2: ПРОВЕРКА СВЯЗИ ================= -->
<div class="layer" id="layerCheck">
<div class="dialog small">
<div class="d-head">
<h2>Проверка связи</h2>
<button class="x-btn" onclick="closeCheck()">✕</button>
</div>
<div class="d-body">
<div class="probe-head" id="probeHead">
<div class="big" id="probeIcon"></div>
<div class="txt">
<div class="ttl" id="probeTitle">Спрашиваю агента…</div>
<div class="sub" id="probeSub">http://192.168.88.135:8080/agentik</div>
</div>
</div>
<div class="probe-list" id="probeList" style="display:none">
<div><span>Ответ на запрос</span><b id="p1">—</b></div>
<div><span>Отвечает по адресу</span><b id="p2">—</b></div>
<div><span>Диалогов у агента</span><b id="p3">—</b></div>
<div><span>Отклик</span><b id="p4">—</b></div>
</div>
<div class="hint-line" id="probeHint" style="display:none"></div>
</div>
<div class="d-foot">
<button class="btn" style="flex:1" id="probeAgain" onclick="runCheck()">Проверить снова</button>
<button class="btn prim" style="flex:1" id="probeOk" disabled>Добавить агента</button>
</div>
</div>
</div>
<script>
function el(id){ return document.getElementById(id); }
// ── переключение список / форма ──
function show(v) {
const isList = (v === 'list');
el('viewList').style.display = isList ? 'block' : 'none';
el('viewAdd').style.display = isList ? 'none' : 'block';
el('footList').style.display = isList ? 'flex' : 'none';
el('footAdd').style.display = isList ? 'none' : 'flex';
el('dTitle').textContent = isList ? 'Агенты' : 'Новый агент';
}
// ── цвет ИЛИ картинка ──
document.querySelectorAll('.appearance-tabs .tab').forEach(tab => tab.onclick = () => {
document.querySelectorAll('.appearance-tabs .tab').forEach(x => x.classList.remove('on'));
tab.classList.add('on');
const pic = (tab.dataset.tab === 'pic');
el('paneColor').classList.toggle('on', !pic);
el('panePic').classList.toggle('on', pic);
});
document.querySelectorAll('.swatch').forEach(s => s.onclick = () => {
document.querySelectorAll('.swatch').forEach(x => x.classList.remove('on'));
s.classList.add('on');
});
// выбор картинки: в настоящем клиенте — системный диалог выбора файла,
// а выбранный файл КОПИРУЕТСЯ в папку клиента (не ссылка на исходный путь)
function pickPic() {
setPic('data:image/svg+xml;utf8,' + encodeURIComponent(
`<svg xmlns="http://www.w3.org/2000/svg" width="54" height="54">
<rect width="54" height="54" fill="#86C99A"/>
<circle cx="27" cy="21" r="8" fill="#121218"/>
<path d="M10 50 Q27 30 44 50 Z" fill="#121218"/>
</svg>`));
}
function setPic(src) {
const p = el('picPreview');
p.innerHTML = `<img src="${src}" alt="">`;
p.classList.add('filled');
el('picClear').style.display = '';
}
function clearPic() {
const p = el('picPreview');
p.innerHTML = '🖼';
p.classList.remove('filled');
el('picClear').style.display = 'none';
}
// ── ОКНО ПРОВЕРКИ: открывается из формы или из списка ──
let checkAddr = '', checkOk = true;
function openCheck(from, addr, ok) {
checkAddr = addr; checkOk = ok;
el('probeOk').textContent = (from === 'form') ? 'Добавить агента' : 'Готово';
el('scrim').classList.add('on');
el('layerCheck').classList.add('on');
runCheck();
}
function closeCheck() {
el('layerCheck').classList.remove('on');
el('scrim').classList.remove('on');
}
function runCheck() {
const list = el('probeList'), hint = el('probeHint');
el('probeHead').className = 'probe-head busy';
el('probeIcon').outerHTML = '<span class="spin" id="probeIcon"></span>';
el('probeTitle').textContent = 'Спрашиваю агента…';
el('probeSub').textContent = checkAddr;
list.style.display = 'none';
hint.style.display = 'none';
el('probeOk').disabled = true;
setTimeout(() => {
document.getElementById('probeIcon').outerHTML = '<div class="big" id="probeIcon"></div>';
el('probeSub').textContent = checkAddr;
if (!checkOk) {
el('probeHead').className = 'probe-head err';
el('probeIcon').textContent = '✕';
el('probeTitle').textContent = 'Агент не отвечает';
el('p1').textContent = 'нет ответа'; el('p2').textContent = '—';
el('p3').textContent = '—'; el('p4').textContent = '5000 мс (истекло)';
list.style.display = 'block'; hint.style.display = 'block';
hint.textContent = 'Проверьте адрес и что агент запущен. Убедитесь, что порт открыт и путь /agentik указан целиком.';
el('probeOk').disabled = true;
} else {
el('probeHead').className = 'probe-head ok';
el('probeIcon').textContent = '✓';
el('probeTitle').textContent = 'Агент отвечает';
el('p1').textContent = 'ok'; el('p2').textContent = checkAddr;
el('p3').textContent = '4'; el('p4').textContent = '38 мс';
list.style.display = 'block'; hint.style.display = 'block';
hint.textContent = 'Адрес рабочий. Можно добавлять этого агента.';
el('probeOk').disabled = false;
}
}, 1400);
}
</script>
</body>
</html>