Core: - ModelCatalog: periodic background sync of provider models + fuzzy resolve. Misspelled model ids map to the nearest match (cheapest on ties); no match raises ModelResolutionError with cheapest-first suggestions for the agent. mcp-server: - nexus_stt now writes the transcript to a Markdown file in the media dir so it appears in the gallery and is readable in the modal. - media handler resolves model via catalog before generating; returns a note when substituting, or a BAD_REQUEST with available models when nothing fits. - context wires a ModelCatalog per media provider with auto-sync. admin: - /media serves correct Content-Type (incl. text/markdown for transcripts). - Gallery: audio play button on cards, dedicated audio player in modal, text/transcript tiles that fetch and render the .md in the modal. - Header: auto-refresh toggle (5/10/30s) that refreshes the active tab without a full page reload; pauses while a modal is open.
225 lines
12 KiB
HTML
225 lines
12 KiB
HTML
<!doctype html>
|
||
<html lang="ru" data-theme="dark">
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>NexusAI Admin</title>
|
||
<style>
|
||
:root[data-theme="dark"] {
|
||
--bg: #0f1115; --panel: #171a21; --panel2: #1f242e; --line: #2a2f3a;
|
||
--text: #e6e9ef; --muted: #9aa4b2; --accent: #4f8cff; --ok: #35c46a;
|
||
--warn: #f0b429; --err: #f26d6d; --shadow: rgba(0,0,0,.5);
|
||
}
|
||
:root[data-theme="light"] {
|
||
--bg: #f4f6fa; --panel: #ffffff; --panel2: #f0f2f7; --line: #dce1ea;
|
||
--text: #1a1f2b; --muted: #5c6675; --accent: #2f6fed; --ok: #1a9e51;
|
||
--warn: #b3810f; --err: #d63d3d; --shadow: rgba(0,0,0,.18);
|
||
}
|
||
* { box-sizing: border-box; }
|
||
body { margin: 0; background: var(--bg); color: var(--text);
|
||
font: 14px/1.5 system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
|
||
transition: background .15s, color .15s; }
|
||
header { padding: 14px 20px; border-bottom: 1px solid var(--line);
|
||
display: flex; align-items: center; gap: 16px; }
|
||
header h1 { font-size: 16px; margin: 0; font-weight: 600; }
|
||
.badge { font-size: 12px; color: var(--muted); }
|
||
header .spacer { flex: 1; }
|
||
#themeToggle { background: var(--panel2); border: 1px solid var(--line); color: var(--text);
|
||
border-radius: 8px; padding: 6px 12px; cursor: pointer; font: inherit; }
|
||
nav { display: flex; gap: 6px; padding: 12px 20px 0; }
|
||
nav button { background: var(--panel); color: var(--muted); border: 1px solid var(--line);
|
||
padding: 8px 14px; border-radius: 8px 8px 0 0; cursor: pointer; }
|
||
nav button.active { background: var(--panel2); color: var(--text); border-bottom-color: var(--panel2); }
|
||
main { padding: 20px; }
|
||
.card { background: var(--panel); border: 1px solid var(--line); border-radius: 10px;
|
||
padding: 16px; margin-bottom: 16px; }
|
||
.grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 14px; }
|
||
.stat { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px; padding: 14px; }
|
||
.stat .n { font-size: 22px; font-weight: 700; }
|
||
.stat .l { color: var(--muted); font-size: 12px; }
|
||
table { width: 100%; border-collapse: collapse; }
|
||
th, td { text-align: left; padding: 8px 10px; border-bottom: 1px solid var(--line); font-size: 13px; }
|
||
th { color: var(--muted); font-weight: 500; }
|
||
input, select { background: var(--panel2); color: var(--text); border: 1px solid var(--line);
|
||
border-radius: 8px; padding: 8px 10px; font: inherit; width: 100%; }
|
||
label { display: block; font-size: 12px; color: var(--muted); margin: 8px 0 4px; }
|
||
button.act { background: var(--accent); color: #fff; border: none; border-radius: 8px;
|
||
padding: 8px 14px; cursor: pointer; font: inherit; }
|
||
button.ghost { background: transparent; color: var(--text); border: 1px solid var(--line);
|
||
border-radius: 8px; padding: 6px 10px; cursor: pointer; }
|
||
button.danger { color: var(--err); border-color: var(--err); }
|
||
button:disabled { opacity: .45; cursor: not-allowed; }
|
||
.row { display: flex; gap: 10px; flex-wrap: wrap; align-items: end; }
|
||
.row > div { flex: 1; min-width: 140px; }
|
||
.pill { display: inline-block; padding: 2px 8px; border-radius: 999px; font-size: 11px; }
|
||
.pill.on { background: rgba(53,196,106,.15); color: var(--ok); }
|
||
.pill.off { background: rgba(242,109,109,.15); color: var(--err); }
|
||
.pill.def { background: rgba(79,140,255,.15); color: var(--accent); }
|
||
.gallery { display: grid; grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); gap: 14px; }
|
||
.tile { background: var(--panel2); border: 1px solid var(--line); border-radius: 10px;
|
||
overflow: hidden; position: relative; }
|
||
.tile .thumb { cursor: pointer; }
|
||
.tile img { width: 100%; height: 150px; object-fit: cover; display: block; background: #000; }
|
||
.tile .meta { padding: 8px 10px; font-size: 12px; }
|
||
.tile .meta .m { color: var(--muted); }
|
||
.tile .del { position: absolute; top: 6px; right: 6px; background: var(--panel);
|
||
border: 1px solid var(--line); color: var(--err); border-radius: 6px; padding: 2px 8px;
|
||
cursor: pointer; font-size: 12px; opacity: .85; }
|
||
.tile .del:hover { opacity: 1; }
|
||
.pager { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; }
|
||
.muted { color: var(--muted); }
|
||
.hidden { display: none; }
|
||
.toast { position: fixed; right: 16px; bottom: 16px; background: var(--panel2);
|
||
border: 1px solid var(--line); padding: 10px 14px; border-radius: 8px; box-shadow: 0 6px 24px var(--shadow); }
|
||
audio { width: 100%; }
|
||
code { background: var(--panel2); padding: 1px 5px; border-radius: 4px; }
|
||
/* Modal */
|
||
.modal-backdrop { position: fixed; inset: 0; background: rgba(0,0,0,.7);
|
||
display: flex; align-items: center; justify-content: center; padding: 24px; z-index: 50; }
|
||
.modal { background: var(--panel); border: 1px solid var(--line); border-radius: 12px;
|
||
max-width: 92vw; max-height: 92vh; overflow: auto; box-shadow: 0 20px 60px var(--shadow); }
|
||
.modal-head { display: flex; align-items: center; gap: 12px; padding: 12px 16px;
|
||
border-bottom: 1px solid var(--line); position: sticky; top: 0; background: var(--panel); }
|
||
.modal-head .spacer { flex: 1; }
|
||
.modal-body { padding: 16px; }
|
||
.modal-body img, .modal-body video { max-width: 88vw; max-height: 72vh; display: block; margin: 0 auto; }
|
||
.modal-body audio { width: 60vw; max-width: 600px; }
|
||
.kv { color: var(--muted); font-size: 12px; margin-top: 10px; word-break: break-all; }
|
||
/* Auto-refresh control */
|
||
.autorefresh { display: flex; align-items: center; gap: 6px; font-size: 12px; color: var(--muted); }
|
||
.autorefresh select { width: auto; padding: 4px 6px; }
|
||
.autorefresh input[type="checkbox"] { width: auto; }
|
||
/* Audio card thumb + play button */
|
||
.audio-thumb, .text-thumb { height: 150px; display: flex; align-items: center; justify-content: center;
|
||
cursor: pointer; background: linear-gradient(135deg, var(--panel2), var(--panel)); }
|
||
.text-thumb.err { background: rgba(242,109,109,.08); }
|
||
.play-btn { width: 56px; height: 56px; border-radius: 50%; border: none; cursor: pointer;
|
||
background: var(--accent); color: #fff; font-size: 20px; line-height: 1;
|
||
box-shadow: 0 6px 18px var(--shadow); transition: transform .1s; }
|
||
.play-btn:hover { transform: scale(1.08); }
|
||
.doc-icon { font-size: 42px; opacity: .8; }
|
||
/* Modal audio player */
|
||
.audio-player { display: flex; flex-direction: column; align-items: center; gap: 16px; padding: 20px; }
|
||
.audio-visual { font-size: 64px; }
|
||
/* Transcript / doc view */
|
||
.doc-view { white-space: pre-wrap; word-break: break-word; font: 13px/1.6 ui-monospace, SFMono-Regular, Menlo, monospace;
|
||
background: var(--panel2); border: 1px solid var(--line); border-radius: 8px; padding: 16px;
|
||
max-width: 80vw; max-height: 70vh; overflow: auto; text-align: left; }
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<header>
|
||
<h1>NexusAI Admin</h1>
|
||
<span class="badge" id="cryptoBadge"></span>
|
||
<span class="spacer"></span>
|
||
<label class="autorefresh" title="Автообновление активной вкладки">
|
||
<input type="checkbox" id="autoRefresh" /> Авто-обновление
|
||
<select id="autoRefreshInterval">
|
||
<option value="5000">5с</option>
|
||
<option value="10000" selected>10с</option>
|
||
<option value="30000">30с</option>
|
||
</select>
|
||
</label>
|
||
<button id="themeToggle" title="Переключить тему">🌙 Тёмная</button>
|
||
</header>
|
||
<nav>
|
||
<button data-tab="usage" class="active">Потребление</button>
|
||
<button data-tab="gallery">Галерея</button>
|
||
<button data-tab="providers">Провайдеры</button>
|
||
</nav>
|
||
<main>
|
||
<section id="tab-usage">
|
||
<div class="card">
|
||
<div class="row" style="margin-bottom:12px">
|
||
<div style="max-width:200px">
|
||
<label>Период</label>
|
||
<select id="statsRange">
|
||
<option value="">Всё время</option>
|
||
<option value="1">1 день</option>
|
||
<option value="7" selected>7 дней</option>
|
||
<option value="30">30 дней</option>
|
||
</select>
|
||
</div>
|
||
</div>
|
||
<div class="grid" id="statCards"></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="margin-top:0">По провайдерам</h3>
|
||
<table id="byProvider"><thead><tr><th>Провайдер</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="margin-top:0">По возможностям</h3>
|
||
<table id="byCapability"><thead><tr><th>Возможность</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="margin-top:0">По дням</h3>
|
||
<table id="byDay"><thead><tr><th>День</th><th>Кол-во</th><th>Стоимость, $</th></tr></thead><tbody></tbody></table>
|
||
</div>
|
||
</section>
|
||
|
||
<section id="tab-gallery" class="hidden">
|
||
<div class="card">
|
||
<div class="row">
|
||
<div style="max-width:220px">
|
||
<label>Фильтр по возможности</label>
|
||
<select id="galleryFilter">
|
||
<option value="">Все</option>
|
||
<option value="image">image</option>
|
||
<option value="tts">tts</option>
|
||
<option value="music">music</option>
|
||
<option value="video">video</option>
|
||
<option value="stt">stt</option>
|
||
<option value="vision">vision</option>
|
||
<option value="search">search</option>
|
||
</select>
|
||
</div>
|
||
<div style="max-width:180px">
|
||
<label>Элементов на странице</label>
|
||
<select id="pageSize">
|
||
<option value="50" selected>50</option>
|
||
<option value="100">100</option>
|
||
<option value="150">150</option>
|
||
<option value="200">200</option>
|
||
</select>
|
||
</div>
|
||
<div style="max-width:120px"><label> </label><button class="ghost" id="galleryReload">Обновить</button></div>
|
||
</div>
|
||
<div class="pager" style="margin-top:12px">
|
||
<button class="ghost" id="prevPage">← Назад</button>
|
||
<span class="muted" id="pageInfo">Страница 1</span>
|
||
<button class="ghost" id="nextPage">Вперёд →</button>
|
||
</div>
|
||
</div>
|
||
<div class="gallery" id="gallery"></div>
|
||
</section>
|
||
|
||
<section id="tab-providers" class="hidden">
|
||
<div class="card">
|
||
<h3 style="margin-top:0">Добавить / обновить провайдера</h3>
|
||
<div class="row">
|
||
<div><label>Имя</label><input id="pName" placeholder="nordrouter" /></div>
|
||
<div><label>Тип</label><select id="pType"></select></div>
|
||
<div><label>Base URL (опц.)</label><input id="pBaseUrl" placeholder="https://nordrouter.com" /></div>
|
||
</div>
|
||
<div class="row">
|
||
<div><label>API ключ (опц., шифруется)</label><input id="pKey" type="password" placeholder="sk-..." /></div>
|
||
<div style="max-width:140px"><label>По умолчанию</label><select id="pDefault"><option value="false">Нет</option><option value="true">Да</option></select></div>
|
||
<div style="max-width:140px"><label>Включён</label><select id="pEnabled"><option value="true">Да</option><option value="false">Нет</option></select></div>
|
||
</div>
|
||
<div style="margin-top:12px"><button class="act" id="pSave">Сохранить</button></div>
|
||
</div>
|
||
<div class="card">
|
||
<h3 style="margin-top:0">Провайдеры</h3>
|
||
<table id="providersTable">
|
||
<thead><tr><th>Имя</th><th>Тип</th><th>Ключ</th><th>Статус</th><th>Действия</th></tr></thead>
|
||
<tbody></tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</main>
|
||
|
||
<div id="modalRoot"></div>
|
||
<script src="app.js"></script>
|
||
</body>
|
||
</html>
|