@@ -0,0 +1,404 @@
<!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 : 100 vh ; padding : 24 px ;
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 : 620 px ; min-width : 620 px ; flex : 0 0 auto ; max-height : 620 px ; background : var ( - - panel ) ;
border : 1 px solid var ( - - line ) ; border-radius : 14 px ; display : flex ; flex-direction : column ;
overflow : hidden ; box-shadow : 0 26 px 80 px rgba ( 0 , 0 , 0 , .7 ) ; position : relative ; z-index : 150 ;
}
. d-head { display : flex ; align-items : center ; gap : 11 px ; padding : 16 px 18 px 14 px ; border-bottom : 1 px solid var ( - - line ) ; }
. d-head h2 { font-size : 16 px ; font-weight : 600 ; flex : 1 ; }
. x-btn { width : 30 px ; height : 30 px ; border-radius : 8 px ; border : none ; background : transparent ;
color : var ( - - dim ) ; cursor : pointer ; font-size : 17 px ; }
. x-btn : hover { background : var ( - - panel2 ) ; color : var ( - - fg ) ; }
. d-body { flex : 1 ; overflow-y : auto ; padding : 14 px 18 px 6 px ; }
. d-foot { padding : 13 px 18 px 16 px ; border-top : 1 px solid var ( - - line ) ; display : flex ; gap : 9 px ; }
/* ── список агентов ── */
. agent-card {
display : flex ; align-items : center ; gap : 12 px ; padding : 11 px 12 px ; border-radius : 11 px ;
background : var ( - - panel2 ) ; margin-bottom : 9 px ; border : 1 px solid transparent ;
}
. agent-card : hover { border-color : var ( - - line ) ; }
. avatar {
width : 40 px ; height : 40 px ; flex : 0 0 40 px ; border-radius : 50 % ; display : grid ; place-items : center ;
font-size : 13 px ; 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 : 14 px ; font-weight : 550 ; }
. ag-addr { font-size : 11.5 px ; color : var ( - - dim ) ; font-family : ui-monospace , "SF Mono" , Menlo , monospace ;
white-space : nowrap ; overflow : hidden ; text-overflow : ellipsis ; margin-top : 3 px ; }
. state { display : flex ; align-items : center ; gap : 5 px ; font-size : 11 px ; color : var ( - - ok ) ; flex : 0 0 auto ; }
. state . d { width : 7 px ; height : 7 px ; 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 : 4 px ; flex : 0 0 auto ; }
. mini-ic { width : 30 px ; height : 30 px ; border-radius : 8 px ; border : none ; background : transparent ;
color : var ( - - dim ) ; cursor : pointer ; font-size : 14 px ; }
. mini-ic : hover { background : var ( - - panel ) ; color : var ( - - fg ) ; }
. mini-ic . del : hover { color : var ( - - err ) ; }
/* ── форма ── */
. field { margin-bottom : 15 px ; }
. field label { display : block ; font-size : 12 px ; color : var ( - - dim ) ; margin-bottom : 6 px ; }
. field input {
width : 100 % ; padding : 10 px 12 px ; border-radius : 10 px ; border : 1 px solid var ( - - line ) ;
background : var ( - - bg ) ; color : var ( - - fg ) ; font-size : 13.5 px ; 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.5 px ; }
. field . note { font-size : 11 px ; color : var ( - - dim ) ; margin-top : 6 px ; }
. note code { background : var ( - - panel2 ) ; padding : 1 px 4 px ; border-radius : 4 px ; font-size : 10.5 px ; }
. addr-row { display : flex ; gap : 8 px ; }
. addr-row input { flex : 1 ; }
/* ── цвет ИЛИ картинка ── */
. appearance-tabs { display : flex ; gap : 6 px ; margin-bottom : 11 px ; }
. appearance-tabs . tab {
padding : 6 px 13 px ; border-radius : 9 px ; font-size : 12.5 px ; color : var ( - - dim ) ;
background : var ( - - panel2 ) ; cursor : pointer ; border : 1 px 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 : 9 px ; }
. swatch { width : 30 px ; height : 30 px ; border-radius : 50 % ; cursor : pointer ; border : 2 px solid transparent ; }
. swatch . on { border-color : var ( - - fg ) ; }
. pic-row { display : flex ; align-items : center ; gap : 13 px ; }
. pic-preview {
width : 54 px ; height : 54 px ; border-radius : 50 % ; flex : 0 0 54 px ; background : var ( - - bg ) ;
border : 1 px dashed var ( - - line ) ; display : grid ; place-items : center ; color : var ( - - dim ) ;
font-size : 20 px ; 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 : 8 px ; flex-wrap : wrap ; }
/* ── кнопки ── */
. btn { border-radius : 10 px ; padding : 10 px 17 px ; font-size : 13 px ; cursor : pointer ; border : 1 px 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 : 8 px 13 px ; font-size : 12.5 px ; }
. btn . wide { width : 100 % ; display : flex ; align-items : center ; justify-content : center ; gap : 8 px ;
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 : 480 px ; min-width : 480 px ; max-height : 420 px ; }
. spin { width : 17 px ; height : 17 px ; border-radius : 50 % ; border : 2 px solid var ( - - line ) ;
border-top-color : var ( - - accent ) ; animation : sp .7 s linear infinite ; flex : 0 0 17 px ; }
@ keyframes sp { to { transform : rotate ( 360 deg ) ; } }
. probe-head { display : flex ; align-items : center ; gap : 13 px ; padding : 4 px 0 14 px ; }
. probe-head . big { width : 40 px ; height : 40 px ; border-radius : 50 % ; display : grid ; place-items : center ;
font-size : 19 px ; flex : 0 0 40 px ; }
. 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 : 14 px ; font-weight : 600 ; }
. probe-head . sub { font-size : 12 px ; color : var ( - - dim ) ; margin-top : 2 px ; }
. probe-list { font-size : 12 px ; }
. probe-list div { display : flex ; justify-content : space-between ; gap : 14 px ; padding : 7 px 0 ;
border-bottom : 1 px 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.5 px ; }
. hint-line { font-size : 11.5 px ; color : var ( - - dim ) ; margin-top : 12 px ; padding : 9 px 11 px ;
background : var ( - - bg ) ; border-radius : 9 px ; border : 1 px 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 >